aboutsummaryrefslogtreecommitdiffstats
path: root/ddms
diff options
context:
space:
mode:
Diffstat (limited to 'ddms')
-rw-r--r--ddms/Android.mk5
-rw-r--r--ddms/MODULE_LICENSE_APACHE20
-rw-r--r--ddms/app/.classpath11
-rw-r--r--ddms/app/.project17
-rw-r--r--ddms/app/Android.mk5
-rw-r--r--ddms/app/README11
-rw-r--r--ddms/app/etc/Android.mk8
-rwxr-xr-xddms/app/etc/ddms84
-rwxr-xr-xddms/app/etc/ddms.bat48
-rw-r--r--ddms/app/etc/manifest.txt1
-rw-r--r--ddms/app/src/Android.mk22
-rw-r--r--ddms/app/src/com/android/ddms/AboutDialog.java153
-rw-r--r--ddms/app/src/com/android/ddms/DebugPortProvider.java163
-rw-r--r--ddms/app/src/com/android/ddms/DeviceCommandDialog.java423
-rw-r--r--ddms/app/src/com/android/ddms/DropdownSelectionListener.java80
-rw-r--r--ddms/app/src/com/android/ddms/Main.java110
-rw-r--r--ddms/app/src/com/android/ddms/PrefsDialog.java545
-rw-r--r--ddms/app/src/com/android/ddms/StaticPortConfigDialog.java394
-rw-r--r--ddms/app/src/com/android/ddms/StaticPortEditDialog.java330
-rw-r--r--ddms/app/src/com/android/ddms/UIThread.java1491
-rw-r--r--ddms/app/src/resources/images/ddms-icon.pngbin0 -> 23410 bytes
-rw-r--r--ddms/app/src/resources/images/ddms-logo.pngbin0 -> 12285 bytes
-rw-r--r--ddms/libs/Android.mk5
-rw-r--r--ddms/libs/ddmlib/.classpath6
-rw-r--r--ddms/libs/ddmlib/.project17
-rw-r--r--ddms/libs/ddmlib/Android.mk4
-rw-r--r--ddms/libs/ddmlib/src/Android.mk11
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java714
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java71
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java1050
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java35
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java222
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Client.java768
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java502
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java147
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java72
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java351
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Device.java385
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java866
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java751
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java767
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java74
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java94
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java76
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java497
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java130
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java305
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java86
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java379
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java89
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java446
-rwxr-xr-xddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java184
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java44
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java29
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java371
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Log.java351
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java780
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java130
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java277
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java73
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java95
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java50
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java32
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java949
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java139
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java461
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java577
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java214
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java347
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java74
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java78
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java247
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java85
-rwxr-xr-xddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java369
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java228
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java76
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java90
-rw-r--r--ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java245
-rw-r--r--ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java248
-rw-r--r--ddms/libs/ddmuilib/.classpath9
-rw-r--r--ddms/libs/ddmuilib/.project17
-rw-r--r--ddms/libs/ddmuilib/Android.mk4
-rw-r--r--ddms/libs/ddmuilib/README11
-rw-r--r--ddms/libs/ddmuilib/src/Android.mk22
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java278
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java487
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java50
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java193
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java33
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java79
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java744
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java1454
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java1294
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java45
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java38
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java86
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java62
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java175
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java1633
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java49
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java73
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java256
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java78
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java258
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java582
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java149
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java128
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java572
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java258
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java42
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java68
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java31
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java31
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java91
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java47
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java167
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java833
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java152
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java243
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java373
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java210
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java53
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java45
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java81
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java34
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java42
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java43
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java73
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java89
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java55
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java422
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java379
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java293
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java177
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java219
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java971
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java955
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java82
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java926
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java628
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java90
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java158
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java353
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java27
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java555
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java1571
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/add.pngbin0 -> 146 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/android.pngbin0 -> 3609 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/backward.pngbin0 -> 136 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/clear.pngbin0 -> 217 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/d.pngbin0 -> 638 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-attach.pngbin0 -> 156 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-error.pngbin0 -> 222 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-wait.pngbin0 -> 156 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/delete.pngbin0 -> 107 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/device.pngbin0 -> 135 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/down.pngbin0 -> 141 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/e.pngbin0 -> 511 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/edit.pngbin0 -> 223 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/empty.pngbin0 -> 75 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/emulator.pngbin0 -> 287 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/file.pngbin0 -> 157 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/folder.pngbin0 -> 123 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/forward.pngbin0 -> 137 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/gc.pngbin0 -> 165 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/halt.pngbin0 -> 197 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/heap.pngbin0 -> 222 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/i.pngbin0 -> 498 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/importBug.pngbin0 -> 191 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/load.pngbin0 -> 163 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/pause.pngbin0 -> 98 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/play.pngbin0 -> 138 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/pull.pngbin0 -> 329 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/push.pngbin0 -> 228 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/save.pngbin0 -> 240 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/thread.pngbin0 -> 121 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/up.pngbin0 -> 134 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/v.pngbin0 -> 587 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/w.pngbin0 -> 681 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/warning.pngbin0 -> 147 bytes
180 files changed, 39390 insertions, 0 deletions
diff --git a/ddms/Android.mk b/ddms/Android.mk
new file mode 100644
index 0000000..82c248e
--- /dev/null
+++ b/ddms/Android.mk
@@ -0,0 +1,5 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMS_LOCAL_DIR := $(call my-dir)
+include $(DDMS_LOCAL_DIR)/libs/Android.mk
+include $(DDMS_LOCAL_DIR)/app/Android.mk
diff --git a/ddms/MODULE_LICENSE_APACHE2 b/ddms/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ddms/MODULE_LICENSE_APACHE2
diff --git a/ddms/app/.classpath b/ddms/app/.classpath
new file mode 100644
index 0000000..2fa1fb7
--- /dev/null
+++ b/ddms/app/.classpath
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry excluding="Makefile|resources/" kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmuilib"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/SdkStatsService"/>
+ <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/Android.mk b/ddms/app/Android.mk
new file mode 100644
index 0000000..3857706
--- /dev/null
+++ b/ddms/app/Android.mk
@@ -0,0 +1,5 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMSAPP_LOCAL_DIR := $(call my-dir)
+include $(DDMSAPP_LOCAL_DIR)/etc/Android.mk
+include $(DDMSAPP_LOCAL_DIR)/src/Android.mk
diff --git a/ddms/app/README b/ddms/app/README
new file mode 100644
index 0000000..cc55ddd
--- /dev/null
+++ b/ddms/app/README
@@ -0,0 +1,11 @@
+Using the Eclipse projects for ddms.
+
+ddms 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. \ No newline at end of file
diff --git a/ddms/app/etc/Android.mk b/ddms/app/etc/Android.mk
new file mode 100644
index 0000000..9d69971
--- /dev/null
+++ b/ddms/app/etc/Android.mk
@@ -0,0 +1,8 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_EXECUTABLES := ddms
+include $(BUILD_HOST_PREBUILT)
+
diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms
new file mode 100755
index 0000000..d809cfc
--- /dev/null
+++ b/ddms/app/etc/ddms
@@ -0,0 +1,84 @@
+#!/bin/sh
+# 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
+
+# Mac OS X needs an additional arg, or you get an "illegal thread" complaint.
+if [ `uname` = "Darwin" ]; then
+ os_opts="-XstartOnFirstThread"
+ #because Java 1.6 is 64 bits only and SWT doesn't support this, we force the usage of java 1.5
+ java_cmd="/System/Library/Frameworks/JavaVM.framework/Versions/1.5/Commands/java"
+else
+ os_opts=
+ java_cmd="java"
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+ jarpath=`cygpath -w "$frameworkdir/$jarfile"`
+ progdir=`cygpath -w "$progdir"`
+else
+ jarpath="$frameworkdir/$jarfile"
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec "$java_cmd" -Xmx256M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.ddms.bindir="$progdir" -jar "$jarpath" "$@"
diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat
new file mode 100755
index 0000000..5da9fb5
--- /dev/null
+++ b/ddms/app/etc/ddms.bat
@@ -0,0 +1,48 @@
+@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
+
+set jarfile=ddms.jar
+set frameworkdir=
+set libdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=lib\
+ set libdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=..\framework\
+ set libdir=..\lib\
+
+: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%
+
+call java %java_debug% -Djava.ext.dirs=%frameworkdir% -Djava.library.path=%libdir% -Dcom.android.ddms.bindir= -jar %jarpath% %*
diff --git a/ddms/app/etc/manifest.txt b/ddms/app/etc/manifest.txt
new file mode 100644
index 0000000..84c8acd
--- /dev/null
+++ b/ddms/app/etc/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.ddms.Main
diff --git a/ddms/app/src/Android.mk b/ddms/app/src/Android.mk
new file mode 100644
index 0000000..a013fa6
--- /dev/null
+++ b/ddms/app/src/Android.mk
@@ -0,0 +1,22 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_RESOURCE_DIRS := resources
+
+LOCAL_JAR_MANIFEST := ../etc/manifest.txt
+LOCAL_JAVA_LIBRARIES := \
+ androidprefs \
+ sdkstats \
+ ddmlib \
+ ddmuilib \
+ swt \
+ org.eclipse.jface_3.2.0.I20060605-1400 \
+ org.eclipse.equinox.common_3.2.0.v20060603 \
+ org.eclipse.core.commands_3.2.0.I20060605-1400
+LOCAL_MODULE := ddms
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/ddms/app/src/com/android/ddms/AboutDialog.java b/ddms/app/src/com/android/ddms/AboutDialog.java
new file mode 100644
index 0000000..2910e5e
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/AboutDialog.java
@@ -0,0 +1,153 @@
+/* //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.ImageHelper;
+
+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-logo.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 ImageHelper.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);
+ label.setText("Dalvik Debug Monitor v" + Main.VERSION);
+ label = new Label(textArea, SWT.NONE);
+ label.setText("Copyright 2007, 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/com/android/ddms/DebugPortProvider.java b/ddms/app/src/com/android/ddms/DebugPortProvider.java
new file mode 100644
index 0000000..89cc190
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/DebugPortProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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.Device;
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+
+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 Device}.
+ * @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(Device, String)
+ */
+ public int getPort(Device 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 = Device.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/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java
new file mode 100644
index 0000000..2a1342e
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java
@@ -0,0 +1,423 @@
+/* //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.Device;
+import com.android.ddmlib.IShellOutputReceiver;
+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.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(Device 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() {
+ public void handleEvent(Event event) {
+ if (!mFinished) {
+ Log.i("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 Device mDevice;
+
+ /**
+ * Constructor; pass in the text widget that will receive the output.
+ * @param device
+ */
+ public Gatherer(Shell shell, Device 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;
+ }
+ }
+
+ mShell.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ updateForResult(mResult);
+ }
+ });
+ }
+
+ /**
+ * Called by executeRemoteCommand().
+ */
+ 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() {
+ public void run() {
+ mText.append(text);
+ }
+ });
+ }
+ catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace(); // not expected
+ }
+ }
+
+ public void flush() {
+ // nothing to flush.
+ }
+
+ /**
+ * Called by executeRemoteCommand().
+ */
+ 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, Device 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.i("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/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/com/android/ddms/DropdownSelectionListener.java
new file mode 100644
index 0000000..99d63ce
--- /dev/null
+++ b/ddms/app/src/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.i("ddms", mDropdown.getText() + " Pressed");
+ }
+ }
+}
+
diff --git a/ddms/app/src/com/android/ddms/Main.java b/ddms/app/src/com/android/ddms/Main.java
new file mode 100644
index 0000000..d63b884
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/Main.java
@@ -0,0 +1,110 @@
+/*
+ * 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 java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.management.RuntimeMXBean;
+
+
+/**
+ * Start the UI and network.
+ */
+public class Main {
+
+ /** User visible version number. */
+ public static final String VERSION = "0.8.1";
+
+ public Main() {
+ }
+
+ /*
+ * If a thread bails with an uncaught exception, bring the whole
+ * thing down.
+ */
+ private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
+ public void uncaughtException(Thread t, Throwable e) {
+ Log.e("ddms", "shutting down due to uncaught exception");
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ Log.e("ddms", sw.toString());
+
+ 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.
+ String os = System.getProperty("os.name"); //$NON-NLS-1$
+ if (os.startsWith("Mac OS")) { //$NON-NLS-1$
+ 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");
+
+ // 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")) {
+ SdkStatsService.ping(args[1], args[2]);
+ return;
+ } else if (args.length > 0) {
+ Log.e("ddms", "Unknown argument: " + args[0]);
+ System.exit(1);
+ }
+
+ // ddms itself is wanted: send a ping for ourselves
+ SdkStatsService.ping("ddms", VERSION); //$NON-NLS-1$
+
+ DebugPortManager.setProvider(DebugPortProvider.getInstance());
+
+ // create the three main threads
+ UIThread ui = UIThread.getInstance();
+
+ try {
+ ui.runUI();
+ } 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);
+ }
+}
diff --git a/ddms/app/src/com/android/ddms/PrefsDialog.java b/ddms/app/src/com/android/ddms/PrefsDialog.java
new file mode 100644
index 0000000..69c48b0
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/PrefsDialog.java
@@ -0,0 +1,545 @@
+/* //device/tools/ddms/src/com/android/ddms/PrefsDialog.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.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.sdkstats.SdkStatsService;
+
+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.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.Link;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Preferences dialog.
+ */
+public final class PrefsDialog {
+
+ // Preference store.
+ private static PreferenceStore mPrefStore;
+
+ // 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 constructor -- do not instantiate.
+ */
+ private PrefsDialog() {}
+
+ /**
+ * Return the PreferenceStore that holds our values.
+ */
+ public static PreferenceStore getStore() {
+ return mPrefStore;
+ }
+
+ /**
+ * Save the prefs to the config file.
+ */
+ public static void save() {
+ try {
+ mPrefStore.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() {
+ assert mPrefStore == null;
+
+ mPrefStore = SdkStatsService.getPreferenceStore();
+
+ if (mPrefStore == 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
+ mPrefStore.addPropertyChangeListener(new ChangeListener());
+
+ // Now we initialize the value of the preference, from the values in the store.
+
+ // First the ddm lib.
+ DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE));
+ DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+ DdmPreferences.setLogLevel(mPrefStore.getString(PREFS_LOG_LEVEL));
+ DdmPreferences.setInitialThreadUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE));
+ DdmPreferences.setInitialHeapUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE));
+
+ // 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-eabi-addr2line"); //$NON-NLS-1$
+
+ String traceview = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$
+ if (traceview != null && traceview.length() != 0) {
+ traceview += File.separator + "traceview"; //$NON-NLS-1$
+ } else {
+ traceview = "traceview"; //$NON-NLS-1$
+ }
+ DdmUiPreferences.setTraceviewLocation(traceview);
+
+ // Now the ddmui lib
+ DdmUiPreferences.setStore(mPrefStore);
+ DdmUiPreferences.setThreadRefreshInterval(mPrefStore.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) {
+ mPrefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE);
+
+ mPrefStore.setDefault(PREFS_SELECTED_DEBUG_PORT,
+ DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT);
+
+ mPrefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true);
+ mPrefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false);
+ mPrefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL,
+ DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL);
+
+ mPrefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$
+ mPrefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$
+
+ mPrefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$
+
+ // choose a default font for the text output
+ FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$
+ mPrefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$
+
+ // layout information.
+ mPrefStore.setDefault(SHELL_X, 100);
+ mPrefStore.setDefault(SHELL_Y, 100);
+ mPrefStore.setDefault(SHELL_WIDTH, 800);
+ mPrefStore.setDefault(SHELL_HEIGHT, 600);
+
+ mPrefStore.setDefault(EXPLORER_SHELL_X, 50);
+ mPrefStore.setDefault(EXPLORER_SHELL_Y, 50);
+
+ mPrefStore.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 {
+ public void propertyChange(PropertyChangeEvent event) {
+ String changed = event.getProperty();
+
+ if (changed.equals(PREFS_DEBUG_PORT_BASE)) {
+ DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE));
+ } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) {
+ DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+ } else if (changed.equals(PREFS_LOG_LEVEL)) {
+ DdmPreferences.setLogLevel((String)event.getNewValue());
+ } else if (changed.equals("textSaveDir")) {
+ mPrefStore.setValue("lastTextSaveDir",
+ (String) event.getNewValue());
+ } else if (changed.equals("imageSaveDir")) {
+ mPrefStore.setValue("lastImageSaveDir",
+ (String) event.getNewValue());
+
+ } else {
+ Log.v("ddms", "Preference change: " + event.getProperty()
+ + ": '" + event.getOldValue()
+ + "' --> '" + event.getNewValue() + "'");
+ }
+ }
+ }
+
+
+ /**
+ * Create and display the dialog.
+ */
+ public static void run(Shell shell) {
+ assert mPrefStore != 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("client", new ClientPrefs());
+ prefMgr.addToRoot(node);
+
+ subNode = new PreferenceNode("panel", new PanelPrefs());
+ //prefMgr.addTo(node.getId(), subNode);
+ prefMgr.addToRoot(subNode);
+
+ node = new PreferenceNode("device", new DevicePrefs());
+ prefMgr.addToRoot(node);
+
+ node = new PreferenceNode("LogCat", new LogCatPrefs());
+ prefMgr.addToRoot(node);
+
+ node = new PreferenceNode("app", new AppPrefs());
+ prefMgr.addToRoot(node);
+
+ node = new PreferenceNode("stats", new UsageStatsPrefs());
+ prefMgr.addToRoot(node);
+
+ PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr);
+ dlg.setPreferenceStore(mPrefStore);
+
+ // run it
+ dlg.open();
+
+ // save prefs
+ try {
+ mPrefStore.save();
+ }
+ catch (IOException ioe) {
+ }
+
+ // discard the stuff we created
+ //prefMgr.dispose();
+ //dlg.dispose();
+ }
+
+ /**
+ * "Client Scan" prefs page.
+ */
+ private static class ClientPrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public ClientPrefs() {
+ super(GRID); // use "grid" layout so edit boxes line up
+ setTitle("Client Scan");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ IntegerFieldEditor ife;
+
+ ife = new PortFieldEditor(PREFS_DEBUG_PORT_BASE,
+ "ADB debugger base:", getFieldEditorParent());
+ addField(ife);
+
+ ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT,
+ "Debug selected VM:", getFieldEditorParent());
+ addField(ife);
+ }
+ }
+
+ /**
+ * "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);
+ }
+ }
+
+ /**
+ * "Device" prefs page.
+ */
+ private static class DevicePrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public DevicePrefs() {
+ super(FLAT); // use "flat" layout
+ setTitle("Device");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ DirectoryFieldEditor dfe;
+ FontFieldEditor ffe;
+
+ 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);
+ }
+ }
+
+ /**
+ * "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() {
+ 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);
+ }
+ }
+
+
+ /**
+ * "Application" prefs page.
+ */
+ private static class AppPrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public AppPrefs() {
+ super(FLAT); // use "flat" layout
+ setTitle("DDMS");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ 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));
+
+ Link text = new Link(mTop, SWT.WRAP);
+ text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ text.setText(SdkStatsService.BODY_TEXT);
+
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ SdkStatsService.openUrl(event.text);
+ }
+ });
+
+ mOptInCheckbox = new BooleanFieldEditor(SdkStatsService.PING_OPT_IN,
+ SdkStatsService.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/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java
new file mode 100644
index 0000000..d00bc7f
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java
@@ -0,0 +1,394 @@
+/*
+ * 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() {
+ 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/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java
new file mode 100644
index 0000000..6330126
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java
@@ -0,0 +1,330 @@
+/*
+ * 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.Device;
+
+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 = Device.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() {
+ 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() {
+ 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() {
+ 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() {
+ 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/com/android/ddms/UIThread.java b/ddms/app/src/com/android/ddms/UIThread.java
new file mode 100644
index 0000000..ff89e2c
--- /dev/null
+++ b/ddms/app/src/com/android/ddms/UIThread.java
@@ -0,0 +1,1491 @@
+/*
+ * 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.Client;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.ILogOutput;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.AllocationPanel;
+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.ImageHelper;
+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.log.event.EventLogPanel;
+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 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.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.MessageBox;
+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 Device} / {@link Client} selection changes.
+ */
+public class UIThread implements IUiSelectionListener {
+ /*
+ * 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;
+
+ public 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_COUNT = 6;
+
+ /** 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"
+ };
+
+ private static final String[] mPanelTips = new String[] {
+ "Client information", "Thread status", "VM heap status",
+ "Native heap status", "Allocation Tracker", "Sysinfo graphs"
+ };
+
+ 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 Device 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 ImageLoader mDdmsImageLoader;
+ private ImageLoader mDdmuiLibImageLoader;
+
+ private final class FilterStorage implements ILogFilterStorageManager {
+
+ 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()]);
+ }
+
+ 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());
+ }
+
+ public boolean requiresDefaultFilter() {
+ return true;
+ }
+ }
+
+
+ private LogPanel mLogPanel;
+
+ 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 class TableFocusListener implements ITableFocusListener {
+
+ private IFocusedTableActivator mCurrentActivator;
+
+ public void focusGained(IFocusedTableActivator activator) {
+ mCurrentActivator = activator;
+ if (mCopyMenuItem.isDisposed() == false) {
+ mCopyMenuItem.setEnabled(true);
+ mSelectAllMenuItem.setEnabled(true);
+ }
+ }
+
+ 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();
+ }
+ }
+
+ }
+
+
+ /**
+ * 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)) {
+ mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel();
+ } else {
+ mPanels[PANEL_NATIVE_HEAP] = null;
+ }
+ mPanels[PANEL_ALLOCATIONS] = new AllocationPanel();
+ mPanels[PANEL_SYSINFO] = new SysinfoPanel();
+ }
+
+ /**
+ * 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.
+ */
+ public void runUI() {
+ Display.setAppName("ddms");
+ mDisplay = new Display();
+ Shell shell = new Shell(mDisplay);
+
+ // create the image loaders for DDMS and DDMUILIB
+ mDdmsImageLoader = new ImageLoader(this.getClass());
+ mDdmuiLibImageLoader = new ImageLoader(DevicePanel.class);
+
+ shell.setImage(ImageHelper.loadImage(mDdmsImageLoader, mDisplay,
+ "ddms-icon.png", //$NON-NLS-1$
+ 100, 50, null));
+
+ Log.setLogOutput(new ILogOutput() {
+ 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() {
+ public void run() {
+ Shell shell = mDisplay.getActiveShell();
+ if (logLevel == LogLevel.ERROR) {
+ MessageDialog.openError(shell, tag, message);
+ } else {
+ MessageDialog.openWarning(shell, tag, message);
+ }
+ }
+ });
+ }
+
+ public void printLog(LogLevel logLevel, String tag, String message) {
+ Log.printLog(logLevel, tag, message);
+ }
+ });
+
+ // [try to] ensure ADB is running
+ String adbLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$
+ if (adbLocation != null && adbLocation.length() != 0) {
+ adbLocation += File.separator + "adb"; //$NON-NLS-1$
+ } else {
+ adbLocation = "adb"; //$NON-NLS-1$
+ }
+
+ AndroidDebugBridge.init(true /* debugger support */);
+ AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */);
+
+ 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();
+ }
+ mLogPanel.stopLogCat(true);
+
+ mDevicePanel.dispose();
+ for (TablePanel panel : mPanels) {
+ if (panel != null) {
+ panel.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() {
+ public void controlMoved(ControlEvent e) {
+ // get the new x/y
+ Rectangle rect = shell.getBounds();
+ // store in pref file
+ PreferenceStore prefs = PrefsDialog.getStore();
+ prefs.setValue(PrefsDialog.SHELL_X, rect.x);
+ prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
+ }
+
+ public void controlResized(ControlEvent e) {
+ // get the new w/h
+ Rectangle rect = shell.getBounds();
+ // store in pref file
+ PreferenceStore prefs = PrefsDialog.getStore();
+ prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width);
+ prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.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() {
+ public void controlMoved(ControlEvent e) {
+ // get the new x/y
+ Rectangle rect = shell.getBounds();
+ // store in pref file
+ PreferenceStore prefs = PrefsDialog.getStore();
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
+ }
+
+ public void controlResized(ControlEvent e) {
+ // get the new w/h
+ Rectangle rect = shell.getBounds();
+ // store in pref file
+ PreferenceStore prefs = PrefsDialog.getStore();
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width);
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height);
+ }
+ });
+ }
+
+ /*
+ * Set the confirm-before-close dialog. TODO: enable/disable in prefs. TODO:
+ * is there any point in having this?
+ */
+ private void setConfirmClose(final Shell shell) {
+ if (true)
+ return;
+
+ shell.addListener(SWT.Close, new Listener() {
+ public void handleEvent(Event event) {
+ int style = SWT.APPLICATION_MODAL | SWT.YES | SWT.NO;
+ MessageBox msgBox = new MessageBox(shell, style);
+ msgBox.setText("Confirm...");
+ msgBox.setMessage("Close DDM?");
+ event.doit = (msgBox.open() == SWT.YES);
+ }
+ });
+ }
+
+ /*
+ * 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");
+ MenuItem helpItem = new MenuItem(menuBar, SWT.CASCADE);
+ helpItem.setText("&Help");
+
+ // 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);
+ Menu helpMenu = new Menu(menuBar);
+ helpItem.setMenu(helpMenu);
+
+ MenuItem item;
+
+ // create File menu items
+ item = new MenuItem(fileMenu, SWT.NONE);
+ item.setText("&Preferences...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ PrefsDialog.run(shell);
+ }
+ });
+
+ 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();
+ }
+ });
+
+ new MenuItem(fileMenu, SWT.SEPARATOR);
+
+ item = new MenuItem(fileMenu, SWT.NONE);
+ item.setText("E&xit\tCtrl-Q");
+ item.setAccelerator('Q' | SWT.CONTROL);
+ 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.COMMAND);
+ 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.COMMAND);
+ 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();
+ }
+ });
+
+ // configure Action items based on current state
+ actionMenu.addMenuListener(new MenuAdapter() {
+ @Override
+ public void menuShown(MenuEvent e) {
+ actionHaltItem.setEnabled(mTBHalt.getEnabled());
+ actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled());
+ }
+ });
+
+ // create Device menu items
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("&Screen capture...\tCTrl-S");
+ item.setAccelerator('S' | SWT.CONTROL);
+ item.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);
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("File Explorer...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ createFileExplorer();
+ }
+ });
+
+ new MenuItem(deviceMenu, SWT.SEPARATOR);
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("Show &process status...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("Dump &device state...");
+ item.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);
+ }
+ });
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("Dump &app state...");
+ item.setEnabled(false);
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("Dump &radio state...");
+ item.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);
+ }
+ });
+
+ item = new MenuItem(deviceMenu, SWT.NONE);
+ item.setText("Run &logcat...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt",
+ shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ // create Help menu items
+ item = new MenuItem(helpMenu, SWT.NONE);
+ item.setText("&Contents...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int style = SWT.APPLICATION_MODAL | SWT.OK;
+ MessageBox msgBox = new MessageBox(shell, style);
+ msgBox.setText("Help!");
+ msgBox.setMessage("Help wanted.");
+ msgBox.open();
+ }
+ });
+
+ new MenuItem(helpMenu, SWT.SEPARATOR);
+
+ item = new MenuItem(helpMenu, SWT.NONE);
+ item.setText("&About...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ AboutDialog dlg = new AboutDialog(shell);
+ dlg.open();
+ }
+ });
+
+ // 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);
+ createBottomPanel(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() {
+ 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);
+ 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
+ mLogPanel.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 thread updates" button
+ mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK);
+ mTBShowThreadUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, 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 "show heap updates" button
+ mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK);
+ mTBShowHeapUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, 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?
+ }
+ }
+ });
+
+ 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(ImageHelper.loadImage(mDdmuiLibImageLoader, display,
+ DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBHalt.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.killSelectedClient();
+ }
+ });
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ // add "cause GC" button
+ mTBCauseGc = new ToolItem(toolBar, SWT.PUSH);
+ mTBCauseGc.setToolTipText("Cause an immediate GC in the target VM");
+ mTBCauseGc.setEnabled(false);
+ mTBCauseGc.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display,
+ DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBCauseGc.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.forceGcOnSelectedClient();
+ }
+ });
+
+ 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() {
+ 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);
+ 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();
+ mClipboard = new Clipboard(display);
+
+ 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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 i = 0 ; i < mLogLevelActions.length; i++) {
+ ToolItemAction a = mLogLevelActions[i];
+ if (a == newAction) {
+ a.setChecked(true);
+
+ // set the log level
+ mLogPanel.setCurrentFilterLogLevel(i+2);
+ } else {
+ a.setChecked(false);
+ }
+ }
+ }
+ });
+
+ newAction.item.setToolTipText(name);
+ newAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(new ImageLoader(LogPanel.class), 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);
+ }
+
+ /*
+ * 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(new ImageLoader(DevicePanel.class), 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(mDdmuiLibImageLoader);
+ 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, 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(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(),
+ "importBug.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ // create the event log panel
+ mEventLogPanel = new EventLogPanel(mDdmuiLibImageLoader);
+
+ // 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");
+ pullAction.item.setImage(mDdmuiLibImageLoader.loadImage("pull.png", mDisplay)); //$NON-NLS-1$
+
+ ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH);
+ pushAction.item.setToolTipText("Push file onto Device");
+ pushAction.item.setImage(mDdmuiLibImageLoader.loadImage("push.png", mDisplay)); //$NON-NLS-1$
+
+ ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH);
+ deleteAction.item.setToolTipText("Delete");
+ deleteAction.item.setImage(mDdmuiLibImageLoader.loadImage("delete.png", mDisplay)); //$NON-NLS-1$
+
+ // device explorer
+ mExplorer = new DeviceExplorer();
+
+ mExplorer.setImages(mDdmuiLibImageLoader.loadImage("file.png", mDisplay), //$NON-NLS-1$
+ mDdmuiLibImageLoader.loadImage("folder.png", mDisplay), //$NON-NLS-1$
+ mDdmuiLibImageLoader.loadImage("android.png", mDisplay), //$NON-NLS-1$
+ null);
+ mExplorer.setActions(pushAction, pullAction, deleteAction);
+
+ 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);
+
+ Composite parent = new Composite(mExplorerShell, SWT.NONE);
+ parent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mExplorer.createPanel(parent);
+ mExplorer.switchDevice(mCurrentDevice);
+
+ mExplorerShell.addShellListener(new ShellListener() {
+ public void shellActivated(ShellEvent e) {
+ // pass
+ }
+
+ public void shellClosed(ShellEvent e) {
+ mExplorer = null;
+ mExplorerShell = null;
+ }
+
+ public void shellDeactivated(ShellEvent e) {
+ // pass
+ }
+
+ public void shellDeiconified(ShellEvent e) {
+ // pass
+ }
+
+ 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() {
+ 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() {
+ 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);
+ } else {
+ // list is empty, disable these
+ mTBShowThreadUpdates.setSelection(false);
+ mTBShowThreadUpdates.setEnabled(false);
+ mTBShowHeapUpdates.setSelection(false);
+ mTBShowHeapUpdates.setEnabled(false);
+ mTBHalt.setEnabled(false);
+ mTBCauseGc.setEnabled(false);
+ }
+ }
+
+ /**
+ * Sent when a new {@link Device} 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
+ */
+ public void selectionChanged(Device selectedDevice, Client selectedClient) {
+ if (mCurrentDevice != selectedDevice) {
+ mCurrentDevice = selectedDevice;
+ for (TablePanel panel : mPanels) {
+ if (panel != null) {
+ panel.deviceSelected(mCurrentDevice);
+ }
+ }
+
+ mEmulatorPanel.deviceSelected(mCurrentDevice);
+ mLogPanel.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();
+ }
+ }
+}
diff --git a/ddms/app/src/resources/images/ddms-icon.png b/ddms/app/src/resources/images/ddms-icon.png
new file mode 100644
index 0000000..167a83b
--- /dev/null
+++ b/ddms/app/src/resources/images/ddms-icon.png
Binary files differ
diff --git a/ddms/app/src/resources/images/ddms-logo.png b/ddms/app/src/resources/images/ddms-logo.png
new file mode 100644
index 0000000..b4708b4
--- /dev/null
+++ b/ddms/app/src/resources/images/ddms-logo.png
Binary files differ
diff --git a/ddms/libs/Android.mk b/ddms/libs/Android.mk
new file mode 100644
index 0000000..c62c6d0
--- /dev/null
+++ b/ddms/libs/Android.mk
@@ -0,0 +1,5 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMSLIBS_LOCAL_DIR := $(call my-dir)
+include $(DDMSLIBS_LOCAL_DIR)/ddmlib/Android.mk
+include $(DDMSLIBS_LOCAL_DIR)/ddmuilib/Android.mk
diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath
new file mode 100644
index 0000000..fb50116
--- /dev/null
+++ b/ddms/libs/ddmlib/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/libs/ddmlib/.project b/ddms/libs/ddmlib/.project
new file mode 100644
index 0000000..fea25c7
--- /dev/null
+++ b/ddms/libs/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/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk
new file mode 100644
index 0000000..a49bdd2
--- /dev/null
+++ b/ddms/libs/ddmlib/Android.mk
@@ -0,0 +1,4 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMLIB_LOCAL_DIR := $(call my-dir)
+include $(DDMLIB_LOCAL_DIR)/src/Android.mk
diff --git a/ddms/libs/ddmlib/src/Android.mk b/ddms/libs/ddmlib/src/Android.mk
new file mode 100644
index 0000000..da07f97
--- /dev/null
+++ b/ddms/libs/ddmlib/src/Android.mk
@@ -0,0 +1,11 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_MODULE := ddmlib
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java
new file mode 100644
index 0000000..42022fe
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java
@@ -0,0 +1,714 @@
+/*
+ * 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 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
+
+ public static final int STD_TIMEOUT = 5000; // standard delay, 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() {
+ // ioSuccess = okay = timeout = false;
+ message = "";
+ }
+
+ public boolean ioSuccess; // read all expected data, no timeoutes
+
+ public boolean okay; // first 4 bytes in response were "OKAY"?
+
+ public boolean timeout; // TODO: implement
+
+ public String message; // diagnostic string
+ }
+
+ /**
+ * 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
+ */
+ public static SocketChannel open(InetSocketAddress adbSockAddr,
+ Device device, int devicePort) throws 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 = createAdbForwardRequest(null, devicePort);
+ // Log.hexDump(req);
+
+ if (write(adbChan, req) == false)
+ throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
+
+ AdbResponse resp = readAdbResponse(adbChan, false);
+ if (!resp.okay)
+ throw new IOException("connection request rejected"); //$NON-NLS-1$
+
+ adbChan.configureBlocking(true);
+ } catch (IOException ioe) {
+ adbChan.close();
+ throw ioe;
+ }
+
+ 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.
+ */
+ public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
+ Device device, int pid) throws 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);
+
+ if (write(adbChan, req) == false)
+ throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.okay)
+ throw new IOException("connection request rejected: " + resp.message); //$NON-NLS-1$
+
+ adbChan.configureBlocking(true);
+ } catch (IOException ioe) {
+ adbChan.close();
+ throw ioe;
+ }
+
+ 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 preceeded 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.
+ */
+ static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
+ throws IOException {
+
+ AdbResponse resp = new AdbResponse();
+
+ byte[] reply = new byte[4];
+ if (read(chan, reply) == false) {
+ return resp;
+ }
+ resp.ioSuccess = true;
+
+ 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"
+ while (readDiagString) {
+ // length string is in next 4 bytes
+ byte[] lenBuf = new byte[4];
+ if (read(chan, lenBuf) == false) {
+ Log.w("ddms", "Expected diagnostic string not found");
+ break;
+ }
+
+ 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];
+ if (read(chan, msg) == false) {
+ Log.w("ddms", "Failed reading diagnostic string, len=" + len);
+ break;
+ }
+
+ resp.message = replyToString(msg);
+ Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
+ + resp.message + "'");
+
+ break;
+ }
+
+ return resp;
+ }
+
+ /**
+ * Retrieve the frame buffer from the device.
+ */
+ public static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
+ throws 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);
+
+ if (write(adbChan, request) == false)
+ throw new IOException("failed asking for frame buffer");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.w("ddms", "Got timeout or unhappy response from ADB fb req: "
+ + resp.message);
+ adbChan.close();
+ return null;
+ }
+
+ reply = new byte[16];
+ if (read(adbChan, reply) == false) {
+ Log.w("ddms", "got partial reply from ADB fb:");
+ Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length);
+ adbChan.close();
+ return null;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(reply);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ imageParams.bpp = buf.getInt();
+ imageParams.size = buf.getInt();
+ imageParams.width = buf.getInt();
+ imageParams.height = buf.getInt();
+
+ Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
+ + imageParams.size + ", width=" + imageParams.width
+ + ", height=" + imageParams.height);
+
+ if (write(adbChan, nudge) == false)
+ throw new IOException("failed nudging");
+
+ reply = new byte[imageParams.size];
+ if (read(adbChan, reply) == false) {
+ Log.w("ddms", "got truncated reply from ADB fb data");
+ adbChan.close();
+ return null;
+ }
+ imageParams.data = reply;
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return imageParams;
+ }
+
+ /**
+ * Execute a command on the device and retrieve the output. The output is
+ * handed to "rcvr" as it arrives.
+ */
+ public static void executeRemoteCommand(InetSocketAddress adbSockAddr,
+ String command, Device device, IShellOutputReceiver rcvr)
+ throws 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$
+ if (write(adbChan, request) == false)
+ throw new IOException("failed submitting shell command");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);
+ throw new IOException("sad result from adb: " + resp.message);
+ }
+
+ byte[] data = new byte[16384];
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ 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 {
+ Thread.sleep(WAIT_TIME * 5);
+ } catch (InterruptedException ie) {
+ }
+ } else {
+ 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}.
+ * @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 IOException
+ */
+ public static void runEventLogService(InetSocketAddress adbSockAddr, Device device,
+ LogReceiver rcvr) throws 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}.
+ * @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 IOException
+ */
+ public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName,
+ LogReceiver rcvr) throws 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);
+ if (write(adbChan, request) == false) {
+ throw new IOException("failed to submit the log command");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected log command: " + 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 fowarding
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ * @throws IOException
+ */
+ public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort,
+ int remotePort) throws IOException {
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ byte[] request = formAdbRequest(String.format(
+ "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
+ device.serialNumber, localPort, remotePort));
+
+ if (write(adbChan, request) == false) {
+ throw new IOException("failed to submit the forward command.");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected command: " + resp.message);
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 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 fowarding
+ * @param localPort the local port of the forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ * @throws IOException
+ */
+ public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort,
+ int remotePort) throws IOException {
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ byte[] request = formAdbRequest(String.format(
+ "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
+ device.serialNumber, localPort, remotePort));
+
+ if (!write(adbChan, request)) {
+ throw new IOException("failed to submit the remove forward command.");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected command: " + resp.message);
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 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).
+ *
+ * @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.
+ * @return "true" if all data was read.
+ * @throws IOException
+ */
+ static boolean read(SocketChannel chan, byte[] data) {
+ try {
+ read(chan, data, -1, STD_TIMEOUT);
+ } catch (IOException e) {
+ Log.d("ddms", "readAll: IOException: " + e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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".
+ * @throws IOException
+ */
+ static void read(SocketChannel chan, byte[] data, int length, int timeout) throws 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.i("ddms", "read: timeout");
+ throw new IOException("timeout");
+ }
+ // 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.
+ * @param chan the opened socket to write to.
+ * @param data the buffer to send.
+ * @return "true" if all data was written.
+ */
+ static boolean write(SocketChannel chan, byte[] data) {
+ try {
+ write(chan, data, -1, STD_TIMEOUT);
+ } catch (IOException e) {
+ Log.e("ddms", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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 IOException
+ */
+ static void write(SocketChannel chan, byte[] data, int length, int timeout)
+ throws 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.i("ddms", "write: timeout");
+ throw new IOException("timeout");
+ }
+ // 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 IOException
+ */
+ static void setDevice(SocketChannel adbChan, Device device)
+ throws 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.serialNumber; //$NON-NLS-1$
+ byte[] device_query = formAdbRequest(msg);
+
+ if (write(adbChan, device_query) == false)
+ throw new IOException("failed submitting device (" + device +
+ ") request to ADB");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.okay)
+ throw new IOException("device (" + device +
+ ") request rejected: " + resp.message);
+ }
+
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java
new file mode 100644
index 0000000..c6d4b50
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/**
+ * Holds an Allocation information.
+ */
+public class AllocationInfo implements Comparable<AllocationInfo>, IStackTraceInfo {
+ private String mAllocatedClass;
+ private int mAllocationSize;
+ private short mThreadId;
+ private StackTraceElement[] mStackTrace;
+
+ /*
+ * Simple constructor.
+ */
+ AllocationInfo(String allocatedClass, int allocationSize,
+ short threadId, StackTraceElement[] stackTrace) {
+ mAllocatedClass = allocatedClass;
+ mAllocationSize = allocationSize;
+ mThreadId = threadId;
+ mStackTrace = stackTrace;
+ }
+
+ /**
+ * 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()
+ */
+ public StackTraceElement[] getStackTrace() {
+ return mStackTrace;
+ }
+
+ public int compareTo(AllocationInfo otherAlloc) {
+ return otherAlloc.mAllocationSize - mAllocationSize;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java
new file mode 100644
index 0000000..795bf88
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java
@@ -0,0 +1,1050 @@
+/*
+ * 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.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.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 final static int ADB_VERSION_MICRO_MIN = 20;
+ private final static int ADB_VERSION_MICRO_MAX = -1;
+
+ private final static Pattern sAdbVersion = Pattern.compile(
+ "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$
+
+ private final static String ADB = "adb"; //$NON-NLS-1$
+ private final static String DDMS = "ddms"; //$NON-NLS-1$
+
+ // Where to find the ADB bridge.
+ final static String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$
+ final static int ADB_PORT = 5037;
+
+ static InetAddress sHostAddr;
+ static InetSocketAddress sSocketAddr;
+
+ static {
+ // built-in local address/port for ADB.
+ try {
+ sHostAddr = InetAddress.getByName(ADB_HOST);
+ sSocketAddr = new InetSocketAddress(sHostAddr, ADB_PORT);
+ } catch (UnknownHostException e) {
+
+ }
+ }
+
+ private static AndroidDebugBridge sThis;
+ private static boolean sClientSupport;
+
+ /** Full path to adb. */
+ private String mAdbOsLocation = null;
+
+ private boolean mVersionCheck;
+
+ private boolean mStarted = false;
+
+ private DeviceMonitor mDeviceMonitor;
+
+ private final static ArrayList<IDebugBridgeChangeListener> sBridgeListeners =
+ new ArrayList<IDebugBridgeChangeListener>();
+ private final static ArrayList<IDeviceChangeListener> sDeviceListeners =
+ new ArrayList<IDeviceChangeListener>();
+ private final static 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 Device} 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(Device 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(Device 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 Device#CHANGE_BUILD_INFO}, {@link Device#CHANGE_STATE},
+ * {@link Device#CHANGE_CLIENT_LIST}
+ */
+ public void deviceChanged(Device 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_INTEREST}, {@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);
+ }
+
+ /**
+ * 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 void init(boolean clientSupport) {
+ sClientSupport = clientSupport;
+
+ MonitorThread monitorThread = MonitorThread.createInstance();
+ monitorThread.start();
+
+ HandleHello.register(monitorThread);
+ HandleAppName.register(monitorThread);
+ HandleTest.register(monitorThread);
+ HandleThread.register(monitorThread);
+ HandleHeap.register(monitorThread);
+ HandleWait.register(monitorThread);
+ }
+
+ /**
+ * Terminates the ddm library. This must be called upon application termination.
+ */
+ public static 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();
+ }
+ }
+
+ /**
+ * Returns whether the ddmlib is setup to support monitoring and interacting with
+ * {@link Client}s running on the {@link Device}s.
+ */
+ static boolean getClientSupport() {
+ return sClientSupport;
+ }
+
+ /**
+ * 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 == false) {
+ 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/>
+ * 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) == false) {
+ 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 Device}
+ * 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) == false) {
+ sDeviceListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes the listener from the collection of listeners who will be notified when a
+ * {@link Device} 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) == false) {
+ 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 Device[] getDevices() {
+ synchronized (sLock) {
+ if (mDeviceMonitor != null) {
+ return mDeviceMonitor.getDevices();
+ }
+ }
+
+ return new Device[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 Device} list has been
+ * built before the call to {@link #getDevices()}.
+ * <p/>The recommended way to get the list of {@link Device} 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.length() == 0) {
+ 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;
+ }
+
+ try {
+ String[] command = new String[2];
+ command[0] = mAdbOsLocation;
+ command[1] = "version"; //$NON-NLS-1$
+ Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation)); //$NON-NLS-1$
+ Process process = Runtime.getRuntime().exec(command);
+
+ ArrayList<String> errorOutput = new ArrayList<String>();
+ ArrayList<String> stdOutput = new ArrayList<String>();
+ int status = grabProcessOutput(process, errorOutput, stdOutput,
+ true /* waitForReaders */);
+
+ 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.
+ Log.logAndDisplay(LogLevel.ERROR, ADB,
+ "Failed to parse the output of 'adb version'"); //$NON-NLS-1$
+ }
+
+ } catch (IOException e) {
+ Log.logAndDisplay(LogLevel.ERROR, ADB,
+ "Failed to get the adb version: " + e.getMessage()); //$NON-NLS-1$
+ } catch (InterruptedException e) {
+ } finally {
+
+ }
+ }
+
+ /**
+ * 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).
+ */
+ 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 == false || startAdb() == false)) {
+ 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.
+ * @return true if success
+ */
+ boolean stop() {
+ // if we haven't started we return false;
+ if (mStarted == false) {
+ return false;
+ }
+
+ // kill the monitoring services
+ mDeviceMonitor.stop();
+ mDeviceMonitor = null;
+
+ if (stopAdb() == false) {
+ 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 == false) {
+ 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 Device}.
+ * <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 Device} 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 Device} 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>Device</code>.
+ * @see #getLock()
+ */
+ void deviceConnected(Device 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 Device}.
+ * <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 Device} 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 Device} 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>Device</code>.
+ * @see #getLock()
+ */
+ void deviceDisconnected(Device 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 Device}.
+ * <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 Device} 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 Device} 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>Device</code>.
+ * @see #getLock()
+ */
+ void deviceChanged(Device 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 Device} 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 Device} 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]));
+ proc = Runtime.getRuntime().exec(command);
+
+ 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 ouput 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 displayStdOut If true this will display stdout as well
+ * @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;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
new file mode 100644
index 0000000..129b312
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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.
+ */
+@SuppressWarnings("serial")
+class BadPacketException extends RuntimeException {
+ public BadPacketException()
+ {
+ super();
+ }
+
+ public BadPacketException(String msg)
+ {
+ super(msg);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java
new file mode 100644
index 0000000..441b024
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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.
+ */
+ 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.getDevice();
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java
new file mode 100644
index 0000000..866d578
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java
@@ -0,0 +1,768 @@
+/*
+ * 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.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 interest change */
+ public static final int CHANGE_DEBUGGER_INTEREST = 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: combination of {@link Client#CHANGE_NAME},
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, and {@link Client#CHANGE_PORT}.
+ */
+ public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_INTEREST | 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 Device} on which this Client is running.
+ */
+ public Device getDevice() {
+ 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.
+ */
+ 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
+ }
+ }
+
+ /**
+ * 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 == false) {
+ 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 informations. 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.i("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.i("ddms",
+ "Good handshake from client, sending HELO to " + mClientData.getPid());
+ JdwpPacket.consumeHandshake(mReadBuffer);
+ mConnState = ST_NEED_DDM_PKT;
+ HandleHello.sendHELO(this, SERVER_PROTOCOL_VERSION);
+ // see if we have another packet in the buffer
+ return getJdwpPacket();
+ case JdwpPacket.HANDSHAKE_BAD:
+ Log.i("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.i("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.i("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.i("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/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java
new file mode 100644
index 0000000..2b46b6f
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java
@@ -0,0 +1,502 @@
+/*
+ * 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.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 final static String PRE_INITIALIZED = "<pre-initialized>"; //$NON-NLS-1$
+
+ /** Debugger connection status: not waiting on one, not connected to one, but accepting
+ * new connections. This is the default value. */
+ public static final int DEBUGGER_DEFAULT = 1;
+ /**
+ * Debugger connection status: the application's VM is paused, waiting for a debugger to
+ * connect to it before resuming. */
+ public static final int DEBUGGER_WAITING = 2;
+ /** Debugger connection status : Debugger is connected */
+ public static final int DEBUGGER_ATTACHED = 3;
+ /** Debugger connection status: The listening port for debugger connection failed to listen.
+ * No debugger will be able to connect. */
+ public static final int DEBUGGER_ERROR = 4;
+
+ /**
+ * 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()
+ */
+ public static final int ALLOCATION_TRACKING_UNKNOWN = -1;
+ /**
+ * Allocation tracking status: the {@link Client} is not tracking allocations. */
+ public static final int ALLOCATION_TRACKING_OFF = 0;
+ /**
+ * Allocation tracking status: the {@link Client} is tracking allocations. */
+ public static final int ALLOCATION_TRACKING_ON = 1;
+
+ /**
+ * Name of the value representing the max size of the heap, in the {@link Map} returned by
+ * {@link #getVmHeapInfo(int)}
+ */
+ public final static 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 final static 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 final static 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 final static String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; // $NON-NLS-1$
+
+ // 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;
+
+ // how interested are we in a debugger?
+ private int mDebuggerInterest;
+
+ // 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 int mAllocationStatus = ALLOCATION_TRACKING_UNKNOWN;
+
+ /**
+ * 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;
+ }
+
+
+ }
+
+
+ /**
+ * Generic constructor.
+ */
+ ClientData(int pid) {
+ mPid = pid;
+
+ mDebuggerInterest = DEBUGGER_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;
+ }
+
+ /**
+ * 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.length() > 0) {
+ /*
+ * 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) == false) {
+ mClientDescription = description;
+ }
+ }
+ }
+
+ /**
+ * Returns the debugger connection status. Possible values are {@link #DEBUGGER_DEFAULT},
+ * {@link #DEBUGGER_WAITING}, {@link #DEBUGGER_ATTACHED}, and {@link #DEBUGGER_ERROR}.
+ */
+ public int getDebuggerConnectionStatus() {
+ return mDebuggerInterest;
+ }
+
+ /**
+ * Sets debugger connection status.
+ */
+ void setDebuggerConnectionStatus(int val) {
+ mDebuggerInterest = val;
+ }
+
+ /**
+ * 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 an {@link Iterator} on {@link NativeLibraryMapInfo} objects.
+ * <p/>
+ * The caller must synchronize on the {@link ClientData} object while iterating.
+ */
+ public synchronized Iterator<NativeLibraryMapInfo> getNativeLibraryMapInfo() {
+ return mNativeLibMapInfo.iterator();
+ }
+
+ synchronized void setAllocationStatus(boolean enabled) {
+ mAllocationStatus = enabled ? ALLOCATION_TRACKING_ON : ALLOCATION_TRACKING_OFF;
+ }
+
+ /**
+ * Returns the allocation tracking status.
+ * @see Client#requestAllocationStatus()
+ */
+ public synchronized int 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;
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java
new file mode 100644
index 0000000..c96d40d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java
@@ -0,0 +1,147 @@
+/*
+ * 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 final static boolean DEFAULT_INITIAL_THREAD_UPDATE = false;
+ /** Default value for heap update flag upon client connection. */
+ public final static boolean DEFAULT_INITIAL_HEAP_UPDATE = false;
+ /** Default value for the selected client debug port */
+ public final static int DEFAULT_SELECTED_DEBUG_PORT = 8700;
+ /** Default value for the debug port base */
+ public final static int DEFAULT_DEBUG_PORT_BASE = 8600;
+ /** Default value for the logcat {@link LogLevel} */
+ public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR;
+
+ 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;
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Non accessible constructor.
+ */
+ private DdmPreferences() {
+ // pass, only static methods in the class.
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java
new file mode 100644
index 0000000..9392127
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Device;
+
+/**
+ * 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(Device 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/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java
new file mode 100644
index 0000000..f30509a
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java
@@ -0,0 +1,351 @@
+/*
+ * 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.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.i("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(ClientData.DEBUGGER_DEFAULT);
+ mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
+ }
+ } 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.i("ddms", "Good handshake from debugger");
+ JdwpPacket.consumeHandshake(mReadBuffer);
+ sendHandshake();
+ mConnState = ST_READY;
+
+ ClientData cd = mClient.getClientData();
+ cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_ATTACHED);
+ mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
+
+ // 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.i("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/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java
new file mode 100644
index 0000000..0e7f0bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java
@@ -0,0 +1,385 @@
+/*
+ * 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.Client;
+import com.android.ddmlib.log.LogReceiver;
+
+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;
+
+
+/**
+ * A Device. It can be a physical device or an emulator.
+ *
+ * TODO: make this class package-protected, and shift all callers to use IDevice
+ */
+public final class Device implements IDevice {
+ /**
+ * The state of a device.
+ */
+ public static enum DeviceState {
+ BOOTLOADER("bootloader"), //$NON-NLS-1$
+ OFFLINE("offline"), //$NON-NLS-1$
+ ONLINE("device"); //$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;
+ }
+ }
+
+ /** Emulator Serial Number regexp. */
+ final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$
+
+ /** Serial number of the device */
+ String serialNumber = null;
+
+ /** Name of the AVD */
+ String mAvdName = null;
+
+ /** State of the device. */
+ DeviceState state = null;
+
+ /** Device properties. */
+ private final Map<String, String> mProperties = new HashMap<String, String>();
+
+ private final ArrayList<Client> mClients = new ArrayList<Client>();
+ private DeviceMonitor mMonitor;
+
+ /**
+ * Socket for the connection monitoring client connection/disconnection.
+ */
+ private SocketChannel mSocketChannel;
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getSerialNumber()
+ */
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public String getAvdName() {
+ return mAvdName;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getState()
+ */
+ public DeviceState getState() {
+ return state;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getProperties()
+ */
+ public Map<String, String> getProperties() {
+ return Collections.unmodifiableMap(mProperties);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getPropertyCount()
+ */
+ public int getPropertyCount() {
+ return mProperties.size();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getProperty(java.lang.String)
+ */
+ public String getProperty(String name) {
+ return mProperties.get(name);
+ }
+
+
+ @Override
+ public String toString() {
+ return serialNumber;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isOnline()
+ */
+ public boolean isOnline() {
+ return state == DeviceState.ONLINE;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isEmulator()
+ */
+ public boolean isEmulator() {
+ return serialNumber.matches(RE_EMULATOR_SN);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isOffline()
+ */
+ public boolean isOffline() {
+ return state == DeviceState.OFFLINE;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isBootLoader()
+ */
+ public boolean isBootLoader() {
+ return state == DeviceState.BOOTLOADER;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#hasClients()
+ */
+ public boolean hasClients() {
+ return mClients.size() > 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClients()
+ */
+ public Client[] getClients() {
+ synchronized (mClients) {
+ return mClients.toArray(new Client[mClients.size()]);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClient(java.lang.String)
+ */
+ public Client getClient(String applicationName) {
+ synchronized (mClients) {
+ for (Client c : mClients) {
+ if (applicationName.equals(c.getClientData().getClientDescription())) {
+ return c;
+ }
+ }
+
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getSyncService()
+ */
+ public SyncService getSyncService() {
+ SyncService syncService = new SyncService(AndroidDebugBridge.sSocketAddr, this);
+ if (syncService.openSync()) {
+ return syncService;
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getFileListingService()
+ */
+ public FileListingService getFileListingService() {
+ return new FileListingService(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getScreenshot()
+ */
+ public RawImage getScreenshot() throws IOException {
+ return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver)
+ */
+ public void executeShellCommand(String command, IShellOutputReceiver receiver)
+ throws IOException {
+ AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this,
+ receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver)
+ */
+ public void runEventLogService(LogReceiver receiver) throws IOException {
+ AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver)
+ */
+ public void runLogService(String logname,
+ LogReceiver receiver) throws IOException {
+ AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#createForward(int, int)
+ */
+ public boolean createForward(int localPort, int remotePort) {
+ try {
+ return AdbHelper.createForward(AndroidDebugBridge.sSocketAddr, this,
+ localPort, remotePort);
+ } catch (IOException e) {
+ Log.e("adb-forward", e); //$NON-NLS-1$
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#removeForward(int, int)
+ */
+ public boolean removeForward(int localPort, int remotePort) {
+ try {
+ return AdbHelper.removeForward(AndroidDebugBridge.sSocketAddr, this,
+ localPort, remotePort);
+ } catch (IOException e) {
+ Log.e("adb-remove-forward", e); //$NON-NLS-1$
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClientName(int)
+ */
+ public String getClientName(int pid) {
+ synchronized (mClients) {
+ for (Client c : mClients) {
+ if (c.getClientData().getPid() == pid) {
+ return c.getClientData().getClientDescription();
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ Device(DeviceMonitor monitor) {
+ mMonitor = monitor;
+ }
+
+ DeviceMonitor getMonitor() {
+ return mMonitor;
+ }
+
+ void addClient(Client client) {
+ synchronized (mClients) {
+ mClients.add(client);
+ }
+ }
+
+ List<Client> getClientList() {
+ return mClients;
+ }
+
+ boolean hasClient(int pid) {
+ synchronized (mClients) {
+ for (Client client : mClients) {
+ if (client.getClientData().getPid() == pid) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ void clearClientList() {
+ synchronized (mClients) {
+ mClients.clear();
+ }
+ }
+
+ /**
+ * Sets the client monitoring socket.
+ * @param socketChannel the sockets
+ */
+ void setClientMonitoringSocket(SocketChannel socketChannel) {
+ mSocketChannel = socketChannel;
+ }
+
+ /**
+ * Returns the client monitoring socket.
+ */
+ SocketChannel getClientMonitoringSocket() {
+ return mSocketChannel;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ void update(int changeMask) {
+ mMonitor.getServer().deviceChanged(this, changeMask);
+ }
+
+ void update(Client client, int changeMask) {
+ mMonitor.getServer().clientChanged(client, changeMask);
+ }
+
+ void addProperty(String label, String value) {
+ mProperties.put(label, value);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java
new file mode 100644
index 0000000..f9d0fa0
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java
@@ -0,0 +1,866 @@
+/*
+ * 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.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.Device.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.Iterator;
+import java.util.List;
+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() == false) {
+ 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 == false) {
+ 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 (IOException ioe) {
+ if (mQuit == false) {
+ Log.e("DeviceMonitor", "Adb connection Error:" + ioe.getMessage());
+ mMonitoring = false;
+ if (mMainAdbConnection != null) {
+ try {
+ mMainAdbConnection.close();
+ } catch (IOException ioe2) {
+ // we can safely ignore that one.
+ }
+ mMainAdbConnection = null;
+ }
+ }
+ }
+ } while (mQuit == false);
+ }
+
+ /**
+ * 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.sSocketAddr);
+ adbChannel.socket().setTcpNoDelay(true);
+ } catch (IOException e) {
+ }
+
+ return adbChannel;
+ }
+
+ /**
+ *
+ * @return
+ * @throws IOException
+ */
+ private boolean sendDeviceListMonitoringRequest() throws IOException {
+ byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$
+
+ if (AdbHelper.write(mMainAdbConnection, request) == false) {
+ Log.e("DeviceMonitor", "Sending Tracking request failed!");
+ mMainAdbConnection.close();
+ throw new IOException("Sending Tracking request failed!");
+ }
+
+ AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection,
+ false /* readDiagString */);
+
+ if (resp.ioSuccess == false) {
+ Log.e("DeviceMonitor", "Failed to read the adb response!");
+ mMainAdbConnection.close();
+ throw new IOException("Failed to read the adb response!");
+ }
+
+ if (resp.okay == false) {
+ // request was refused by adb!
+ Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+ }
+
+ return resp.okay;
+ }
+
+ /**
+ * 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);
+ device.serialNumber = param[0];
+ device.state = 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()) {
+ 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.serialNumber.equals(device.serialNumber)) {
+ foundMatch = true;
+
+ // update the state if needed.
+ if (device.state != newDevice.state) {
+ device.state = newDevice.state;
+ device.update(Device.CHANGE_STATE);
+
+ // if the device just got ready/online, we need to start
+ // monitoring it.
+ if (device.isOnline()) {
+ if (AndroidDebugBridge.getClientSupport() == true) {
+ if (startMonitoringDevice(device) == false) {
+ Log.e("DeviceMonitor",
+ "Failed to start monitoring "
+ + device.serialNumber);
+ }
+ }
+
+ if (device.getPropertyCount() == 0) {
+ queryNewDeviceForInfo(device);
+ }
+ }
+ }
+
+ // remove the new device from the list since it's been used
+ newList.remove(dd);
+ break;
+ }
+ }
+
+ if (foundMatch == false) {
+ // 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() == true) {
+ if (newDevice.isOnline()) {
+ startMonitoringDevice(newDevice);
+ }
+ }
+
+ // look for their build info.
+ if (newDevice.isOnline()) {
+ queryNewDeviceForInfo(newDevice);
+ }
+ }
+ }
+ }
+ 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));
+
+ // now get the emulator Virtual Device name (if applicable).
+ if (device.isEmulator()) {
+ EmulatorConsole console = EmulatorConsole.getConsole(device);
+ if (console != null) {
+ device.mAvdName = console.getAvdName();
+ }
+ }
+ } catch (IOException e) {
+ // if we can't get the build info, it doesn't matter too much
+ }
+ }
+
+ /**
+ * 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 (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.size() > 0) {
+ Set<Client> clients = mClientsToReopen.keySet();
+ MonitorThread monitorThread = MonitorThread.getInstance();
+
+ for (Client client : clients) {
+ Device device = client.getDevice();
+ 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) {
+ if (mQuit == false) {
+
+ }
+ }
+
+ } while (mQuit == false);
+ }
+
+ private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device)
+ throws IOException {
+
+ AdbHelper.setDevice(socket, device);
+
+ byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$
+
+ if (AdbHelper.write(socket, request) == false) {
+ Log.e("DeviceMonitor", "Sending jdwp tracking request failed!");
+ socket.close();
+ throw new IOException();
+ }
+
+ AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */);
+
+ if (resp.ioSuccess == false) {
+ Log.e("DeviceMonitor", "Failed to read the adb response!");
+ socket.close();
+ throw new IOException();
+ }
+
+ if (resp.okay == false) {
+ // request was refused by adb!
+ Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+ }
+
+ return resp.okay;
+ }
+
+ private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length)
+ throws IOException {
+ if (length >= 0) {
+ // array for the current pids.
+ ArrayList<Integer> pidList = new ArrayList<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 {
+ pidList.add(Integer.valueOf(pid));
+ } catch (NumberFormatException nfe) {
+ // looks like this pid is not really a number. Lets ignore it.
+ continue;
+ }
+ }
+ }
+
+ MonitorThread monitorThread = MonitorThread.getInstance();
+
+ // Now we merge the current list with the old one.
+ // this is the same mechanism as the merging of the device list.
+
+ // For each client in the current list, we look for a matching the pid in the new list.
+ // * if we find it, we do nothing, except removing the pid from its list,
+ // to mark it as "processed"
+ // * if we do not find any match, we remove the client from the current list.
+ // Once this is done, the new list contains pids for which we don't have clients yet,
+ // so we create clients for them, add them to the list, and start monitoring them.
+
+ List<Client> clients = device.getClientList();
+
+ boolean changed = false;
+
+ // because MonitorThread#dropClient acquires first the monitorThread lock and then the
+ // Device client list lock (when removing the Client from the list), we have to make
+ // sure we acquire the locks in the same order, since another thread (MonitorThread),
+ // could call dropClient itself.
+ synchronized (monitorThread) {
+ synchronized (clients) {
+ for (int c = 0 ; c < clients.size() ;) {
+ Client client = clients.get(c);
+ int pid = client.getClientData().getPid();
+
+ // look for a matching pid
+ Integer match = null;
+ for (Integer matchingPid : pidList) {
+ if (pid == matchingPid.intValue()) {
+ match = matchingPid;
+ break;
+ }
+ }
+
+ if (match != null) {
+ pidList.remove(match);
+ c++; // move on to the next client.
+ } else {
+ // we need to drop the client. the client will remove itself from the
+ // list of its device which is 'clients', so there's no need to
+ // increment c.
+ // We ask the monitor thread to not send notification, as we'll do
+ // it once at the end.
+ monitorThread.dropClient(client, false /* notify */);
+ changed = true;
+ }
+ }
+ }
+ }
+
+ // at this point whatever pid is left in the list needs to be converted into Clients.
+ for (int newPid : pidList) {
+ openClient(device, newPid, getNextDebuggerPort(), monitorThread);
+ changed = true;
+ }
+
+ if (changed) {
+ 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.sSocketAddr, device, pid);
+
+ // required for Selector
+ clientSocket.configureBlocking(false);
+ } catch (UnknownHostException uhe) {
+ Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid);
+ 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);
+ }
+ client.requestAllocationStatus();
+ } catch (IOException ioe) {
+ client.getClientData().setDebuggerConnectionStatus(ClientData.DEBUGGER_ERROR);
+ Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger");
+ // oh well
+ }
+ } 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.size() > 0) {
+ int port = mDebuggerPorts.get(0);
+
+ // remove it.
+ mDebuggerPorts.remove(0);
+
+ // if there's nothing left, add the next port to the list
+ if (mDebuggerPorts.size() == 0) {
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java
new file mode 100644
index 0000000..f3986ed
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java
@@ -0,0 +1,751 @@
+/*
+ * 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.Calendar;
+import java.util.HashMap;
+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 final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
+
+ private final static int WAIT_TIME = 5; // spin-wait sleep, in ms
+
+ private final static int STD_TIMEOUT = 5000; // standard delay, in ms
+
+ private final static String HOST = "127.0.0.1"; //$NON-NLS-1$
+
+ private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GPS =
+ "geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$
+ "%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$
+ "1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$
+
+ private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
+
+ /**
+ * Array of delay values: no delay, gprs, edge/egprs, umts/3d
+ */
+ public final static 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 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 final static 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 final static 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 final static String RESULT_OK = null;
+
+ private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
+ private final static Pattern sVoiceStatusRegexp = Pattern.compile(
+ "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sDataStatusRegexp = Pattern.compile(
+ "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sDownloadSpeedRegexp = Pattern.compile(
+ "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sMinLatencyRegexp = Pattern.compile(
+ "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+ private final static 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(Device d) {
+ // we need to make sure that the device is an emulator
+ Matcher m = sEmulatorRegexp.matcher(d.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 null;
+ }
+ } 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;
+ }
+
+ EmulatorConsole console = sEmulators.get(port);
+
+ if (console != null) {
+ // if the console exist, we ping the emulator to check the connection.
+ if (console.ping() == false) {
+ 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 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 agains 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());
+
+ // 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());
+
+ // 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) {
+
+ Calendar c = Calendar.getInstance();
+
+ double absLong = Math.abs(longitude);
+ int longDegree = (int)Math.floor(absLong);
+ char longDirection = 'E';
+ if (longitude < 0) {
+ longDirection = 'W';
+ }
+
+ double longMinute = (absLong - Math.floor(absLong)) * 60;
+
+ double absLat = Math.abs(latitude);
+ int latDegree = (int)Math.floor(absLat);
+ char latDirection = 'N';
+ if (latitude < 0) {
+ latDirection = 'S';
+ }
+
+ double latMinute = (absLat - Math.floor(absLat)) * 60;
+
+ String command = String.format(COMMAND_GPS,
+ c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE),
+ c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND),
+ latDegree, latMinute, latDirection,
+ longDegree, longMinute, longDirection);
+
+ return processCommand(command);
+ }
+
+ /**
+ * 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, AdbHelper.STD_TIMEOUT);
+
+ result = true;
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (result == false) {
+ // 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 == false) {
+ 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) {
+ if (mBuffer[currentPosition-1] == '\n' &&
+ mBuffer[currentPosition-2] == '\r' &&
+ mBuffer[currentPosition-3] == 'K' &&
+ mBuffer[currentPosition-4] == 'O') {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java
new file mode 100644
index 0000000..b50cf79
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java
@@ -0,0 +1,767 @@
+/*
+ * 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 final static Pattern sApkPattern =
+ Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+ private final static 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 final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$
+
+ /** Top level data folder. */
+ public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$
+ /** Top level sdcard folder. */
+ public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$
+ /** Top level system folder. */
+ public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$
+ /** Top level temp folder. */
+ public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$
+ /** Application folder. */
+ public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$
+
+ private final static String[] sRootLevelApprovedItems = {
+ DIRECTORY_DATA,
+ DIRECTORY_SDCARD,
+ DIRECTORY_SYSTEM,
+ DIRECTORY_TEMP
+ };
+
+ 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 Pattern sLsPattern = 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 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 final static class FileEntry {
+ /** Pattern to escape filenames for shell command consumption. */
+ private final static Pattern sEscapePattern = Pattern.compile(
+ "([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$
+
+ /**
+ * Comparator object for FileEntry
+ */
+ private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() {
+ public int compare(FileEntry o1, FileEntry o2) {
+ if (o1 instanceof FileEntry && o2 instanceof FileEntry) {
+ FileEntry fe1 = (FileEntry)o1;
+ FileEntry fe2 = (FileEntry)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 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 true if the entry is a directory, false otherwise;
+ */
+ public int getType() {
+ return 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();
+ if (current-fetchTime > REFRESH_TEST) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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
+ */
+ private String escape(String entryName) {
+ return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$
+ }
+ }
+
+ private 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.length() == 0) {
+ continue;
+ }
+
+ // run the line through the regexp
+ Matcher m = sLsPattern.matcher(line);
+ if (m.matches() == false) {
+ continue;
+ }
+
+ // get the name
+ String name = m.group(7);
+
+ // if the parent is root, we only accept selected items
+ if (mParentEntry.isRoot()) {
+ boolean found = false;
+ for (String approved : sRootLevelApprovedItems) {
+ if (approved.equals(name)) {
+ found = true;
+ break;
+ }
+ }
+
+ // if it's not in the approved list we skip this entry.
+ if (found == false) {
+ continue;
+ }
+ }
+
+ // 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;
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public void finishLinks() {
+ // TODO Handle links in the listing service
+ }
+ }
+
+ /**
+ * 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() == false) {
+ 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.length() > 0) {
+ // 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);
+ }
+ }
+ }
+ }
+ }
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException 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.size() > 0) {
+ 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;
+ }
+
+ private void doLs(FileEntry entry) {
+ // 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.getFullPath(); //$NON-NLS-1$
+
+ // 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();
+ } catch (IOException e) {
+ }
+
+
+ // 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/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java
new file mode 100644
index 0000000..9293379
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java
@@ -0,0 +1,74 @@
+/*
+ * 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 {
+ final static String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$
+
+ private final static 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.length() == 0 || line.startsWith("#")) {
+ continue;
+ }
+
+ Matcher m = GETPROP_PATTERN.matcher(line);
+ if (m.matches()) {
+ String label = m.group(1);
+ String value = m.group(2);
+
+ if (label.length() > 0) {
+ mDevice.addProperty(label, value);
+ }
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void done() {
+ mDevice.update(Device.CHANGE_BUILD_INFO);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java
new file mode 100644
index 0000000..99bd4d0
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java
@@ -0,0 +1,94 @@
+/*
+ * 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 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);
+
+ Log.i("ddm-appname", "APNM: app='" + appName + "'");
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ cd.setClientDescription(appName);
+ }
+
+ client = checkDebuggerPortForAppName(client, appName);
+
+ if (client != null) {
+ client.update(Client.CHANGE_NAME);
+ }
+ }
+ }
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java
new file mode 100644
index 0000000..adeedbb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java
new file mode 100644
index 0000000..5752b86
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java
@@ -0,0 +1,497 @@
+/*
+ * 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;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * 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_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_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);
+ client.update(Client.CHANGE_HEAP_DATA);
+ } else if (type == CHUNK_HPST) {
+ handleHPST(client, data);
+ } else if (type == CHUNK_HPEN) {
+ handleHPEN(client, data);
+ client.update(Client.CHANGE_HEAP_DATA);
+ } else if (type == CHUNK_HPSG) {
+ handleHPSG(client, data);
+ } else if (type == CHUNK_REAQ) {
+ handleREAQ(client, data);
+ client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS);
+ } else if (type == CHUNK_REAL) {
+ handleREAL(client, data);
+ client.update(Client.CHANGE_HEAP_ALLOCATIONS);
+ } 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);
+ }
+ } 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();
+ }
+
+ /*
+ * 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 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);
+ }
+
+ /**
+ * 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);
+ 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(classNames[classNameIndex],
+ totalSize, (short) threadId, steArray));
+ }
+
+ // sort biggest allocations first.
+ Collections.sort(list);
+
+ client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries]));
+ }
+
+ /*
+ * 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/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java
new file mode 100644
index 0000000..5ba5aeb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.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.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "hello" chunk (HELO).
+ */
+final class HandleHello extends ChunkHandler {
+
+ public static final int CHUNK_HELO = ChunkHandler.type("HELO");
+
+ 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);
+ }
+
+ /**
+ * 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 {
+ 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);
+
+ 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);
+ } 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);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java
new file mode 100644
index 0000000..ca26590
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.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.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_GC);
+ 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 = ((long)buffer.getInt()) & 0x00000000ffffffffL;
+
+ 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 = (int) buffer.get();
+ long startAddress = (long) 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 = (int)buffer.get() & 0x000000ff;
+ int eLen = ((int)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);
+
+ /*
+ * only check for library addresses as defined in
+ * //device/config/prelink-linux-arm.map
+ */
+ if (tmpStart >= 0x0000000080000000L && tmpStart <= 0x00000000BFFFFFFFL) {
+
+ int index = line.indexOf('/');
+
+ if (index == -1)
+ continue;
+
+ String tmpLib = line.substring(index);
+
+ if (library == null ||
+ (library != null && tmpLib.equals(library) == false)) {
+
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java
new file mode 100644
index 0000000..b9f3a74
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java
new file mode 100644
index 0000000..572eed2
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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 mThreadStatusReqRunning = false;
+ private static volatile boolean mThreadStackTraceReqRunning = 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.i("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.i("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.i("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 (mThreadStatusReqRunning) {
+ Log.w("ddms", "Waiting for previous thread update req to finish");
+ return;
+ }
+
+ new Thread("Thread Status Req") {
+ @Override
+ public void run() {
+ mThreadStatusReqRunning = true;
+ try {
+ sendTHST(client);
+ } catch (IOException ioe) {
+ Log.i("ddms", "Unable to request thread updates from "
+ + client + ": " + ioe.getMessage());
+ } finally {
+ mThreadStatusReqRunning = false;
+ }
+ }
+ }.start();
+ }
+ }
+
+ static void requestThreadStackCallRefresh(final Client client, final int threadId) {
+ if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
+ if (mThreadStackTraceReqRunning ) {
+ Log.w("ddms", "Waiting for previous thread stack call req to finish");
+ return;
+ }
+
+ new Thread("Thread Status Req") {
+ @Override
+ public void run() {
+ mThreadStackTraceReqRunning = true;
+ try {
+ sendSTKL(client, threadId);
+ } catch (IOException ioe) {
+ Log.i("ddms", "Unable to request thread stack call updates from "
+ + client + ": " + ioe.getMessage());
+ } finally {
+ mThreadStackTraceReqRunning = 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/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java
new file mode 100644
index 0000000..d27e636
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java
@@ -0,0 +1,89 @@
+/*
+ * 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 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.i("ddm-wait", "WAIT: reason=" + reason);
+
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_WAITING);
+ }
+
+ client.update(Client.CHANGE_DEBUGGER_INTEREST);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java
new file mode 100644
index 0000000..6a62e60
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java
@@ -0,0 +1,446 @@
+/*
+ * 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 int SOLIDITY_FREE = 0;
+
+ /** The element is strongly-reachable. */
+ public static int SOLIDITY_HARD = 1;
+
+ /** The element is softly-reachable. */
+ public static int SOLIDITY_SOFT = 2;
+
+ /** The element is weakly-reachable. */
+ public static int SOLIDITY_WEAK = 3;
+
+ /** The element is phantom-reachable. */
+ public static int SOLIDITY_PHANTOM = 4;
+
+ /** The element is pending finalization. */
+ public static int SOLIDITY_FINALIZABLE = 5;
+
+ /** The element is not reachable, and is about to be swept/freed. */
+ public static int SOLIDITY_SWEEP = 6;
+
+ /** The reachability of the object is unknown. */
+ public static int SOLIDITY_INVALID = -1;
+
+
+ /*
+ * Kind values, which must match the values in
+ * the HPSG data.
+ */
+
+ /** The element describes a data object. */
+ public static int KIND_OBJECT = 0;
+
+ /** The element describes a class object. */
+ public static int KIND_CLASS_OBJECT = 1;
+
+ /** The element describes an array of 1-byte elements. */
+ public static int KIND_ARRAY_1 = 2;
+
+ /** The element describes an array of 2-byte elements. */
+ public static int KIND_ARRAY_2 = 3;
+
+ /** The element describes an array of 4-byte elements. */
+ public static int KIND_ARRAY_4 = 4;
+
+ /** The element describes an array of 8-byte elements. */
+ public static int KIND_ARRAY_8 = 5;
+
+ /** The element describes an unknown type of object. */
+ public static int KIND_UNKNOWN = 6;
+
+ /** The element describes a native object. */
+ public static int KIND_NATIVE = 7;
+
+ /** The object kind is unknown or unspecified. */
+ public static 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 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 = (int)data.get() & 0x000000ff;
+ int eLen = ((int)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 = (int)data.get() & 0x000000ff;
+ if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
+ throw new ParseException("State mismatch", data.position());
+ }
+ eState = nextState;
+ eLen += ((int)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;
+ }
+
+ 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 final static 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 = (int) hpsgData.get();
+ mStartAddress = (long) 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();
+ }
+
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java
new file mode 100755
index 0000000..5dbce92
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java
@@ -0,0 +1,184 @@
+/*
+ * 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.Device.DeviceState;
+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 final static String PROP_BUILD_VERSION = "ro.build.version.release";
+ public final static String PROP_BUILD_VERSION_NUMBER = "ro.build.version.sdk";
+ public final static String PROP_DEBUGGABLE = "ro.debuggable";
+ /** Serial number of the first connected emulator. */
+ public final static 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;
+
+ /**
+ * 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>&lt;build&gt;</code>".
+ * @return the name of the AVD or <code>null</code> if there isn't any.
+ */
+ public String getAvdName();
+
+ /**
+ * 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 a property value.
+ * @param name the name of the value to return.
+ * @return the value or <code>null</code> if the property does not exist.
+ */
+ public String getProperty(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.
+ */
+ public SyncService getSyncService();
+
+ /**
+ * 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 IOException
+ */
+ public RawImage getScreenshot() throws IOException;
+
+ /**
+ * Executes a shell command on the device, and sends the result to a receiver.
+ * @param command The command to execute
+ * @param receiver The receiver object getting the result from the command.
+ * @throws IOException
+ */
+ public void executeShellCommand(String command,
+ IShellOutputReceiver receiver) throws IOException;
+
+ /**
+ * Runs the event log service and outputs the event log to the {@link LogReceiver}.
+ * @param receiver the receiver to receive the event log entries.
+ * @throws IOException
+ */
+ public void runEventLogService(LogReceiver receiver) throws IOException;
+
+ /**
+ * Runs the log service for the given log and outputs the log to the {@link LogReceiver}.
+ * @param logname the logname of the log to read from.
+ * @param receiver the receiver to receive the event log entries.
+ * @throws IOException
+ */
+ public void runLogService(String logname, LogReceiver receiver) throws IOException;
+
+ /**
+ * Creates a port forwarding between a local and a remote port.
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ */
+ public boolean createForward(int localPort, int remotePort);
+
+ /**
+ * Removes a port forwarding between a local and a remote port.
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ */
+ public boolean removeForward(int localPort, int remotePort);
+
+ /**
+ * 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);
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java
new file mode 100644
index 0000000..fb671bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java
new file mode 100644
index 0000000..3b9d730
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
new file mode 100644
index 0000000..92bbb82
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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 mSerialId = 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 mSerialId++;
+ }
+
+ /**
+ * 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/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java
new file mode 100644
index 0000000..ce95b04
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java
@@ -0,0 +1,351 @@
+/*
+ * 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;
+
+/**
+ * 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.length() > 0) {
+ 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 mLevel = 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) {
+ mLevel = 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() >= mLevel.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) {
+ long msec;
+
+ msec = System.currentTimeMillis();
+ String outMessage = String.format("%02d:%02d %c/%s: %s\n",
+ (msec / 60000) % 60, (msec / 1000) % 60,
+ logLevel.getPriorityLetter(), tag, message);
+ System.out.print(outMessage);
+ }
+
+}
+
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java
new file mode 100644
index 0000000..79eb5bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java
@@ -0,0 +1,780 @@
+/*
+ * 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 mInstance;
+
+ /**
+ * 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 mInstance = new MonitorThread();
+ }
+
+ /**
+ * Get singleton instance of the client monitor thread.
+ */
+ static MonitorThread getInstance() {
+ return mInstance;
+ }
+
+
+ /**
+ * Sets or changes the port number for "debug selected".
+ */
+ synchronized void setDebugSelectedPort(int port) throws IllegalStateException {
+ if (mInstance == null) {
+ return;
+ }
+
+ if (AndroidDebugBridge.getClientSupport() == false) {
+ 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 (mInstance == 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[0]);
+ }
+ }
+
+ /**
+ * Register "handler" as the handler for type "type".
+ */
+ synchronized void registerChunkHandler(int type, ChunkHandler handler) {
+ if (mInstance == 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() == false || key.isValid() == false) {
+ 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 (mInstance == null) {
+ return;
+ }
+
+ synchronized (mClientList) {
+ if (mClientList.remove(client) == false) {
+ 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();
+ }
+
+ /*
+ * 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.i("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.i("ddms", "Closing connection to debugger " + dbg);
+ dbg.closeData();
+ Client client = dbg.getClient();
+ if (client.isDdmAware()) {
+ // TODO: soft-disconnect DDM-aware clients
+ Log.i("ddms", " (recycling client connection as well)");
+
+ // we should drop the client, but also attempt to reopen it.
+ // This is done by the DeviceMonitor.
+ client.getDevice().getMonitor().addClientToDropAndReopen(client,
+ IDebugPortProvider.NO_STATIC_PORT);
+ } else {
+ Log.i("ddms", " (recycling client connection as well)");
+ // we should drop the client, but also attempt to reopen it.
+ // This is done by the DeviceMonitor.
+ client.getDevice().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();
+ }
+
+ mInstance = 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 (mInstance == 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.i("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/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java
new file mode 100644
index 0000000..24dbb05
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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 hether 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)
+ */
+ public final void addOutput(byte[] data, int offset, int length) {
+ if (isCancelled() == false) {
+ String s = null;
+ try {
+ s = new String(data, offset, length, "ISO-8859-1"); //$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 (s != null) {
+ // 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.size() > 0) {
+ // 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()
+ */
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java
new file mode 100644
index 0000000..956b004
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java
@@ -0,0 +1,277 @@
+/*
+ * 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.Iterator;
+import java.util.List;
+
+/**
+ * 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 {
+ /* constants for flag bits */
+ private static final int FLAG_ZYGOTE_CHILD = (1<<31);
+ private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD);
+
+ /**
+ * list of alloc functions that are filtered out when attempting to display
+ * a relevant method responsible for an allocation
+ */
+ private static ArrayList<String> sAllocFunctionFilter;
+ static {
+ sAllocFunctionFilter = new ArrayList<String>();
+ sAllocFunctionFilter.add("malloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("calloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("realloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("get_backtrace"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("get_hash"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("??"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("internal_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("operator new"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("leak_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("chk_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("chk_memalign"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("Malloc"); //$NON-NLS-1$
+ }
+
+ 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
+ */
+ 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.
+ */
+ 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 Long[] getStackCallAddresses() {
+ return mStackCallAddresses.toArray(new Long[mStackCallAddresses.size()]);
+ }
+
+ /**
+ * 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.size() != 0;
+ }
+
+ /**
+ * Returns the resolved stack call.
+ * @return An array of {@link NativeStackCallInfo} or <code>null</code> if the stack call
+ * was not resolved.
+ * @see #setResolvedStackCall(ArrayList)
+ * @see #isStackCallResolved()
+ */
+ public synchronized NativeStackCallInfo[] getResolvedStackCall() {
+ if (mIsStackCallResolved) {
+ return mResolvedStackCall.toArray(new NativeStackCallInfo[mResolvedStackCall.size()]);
+ }
+
+ 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;
+ }
+
+ /**
+ * Returns a string representation of the object.
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append("Allocations: ");
+ buffer.append(mAllocations);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ buffer.append("Size: ");
+ buffer.append(mSize);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ buffer.append("Total Size: ");
+ buffer.append(mSize * mAllocations);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ Iterator<Long> addrIterator = mStackCallAddresses.iterator();
+ Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
+
+ while (sourceIterator.hasNext()) {
+ long addr = addrIterator.next();
+ NativeStackCallInfo source = sourceIterator.next();
+ 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()));
+ }
+ }
+
+ 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(ArrayList)
+ * @see #isStackCallResolved()
+ */
+ public synchronized NativeStackCallInfo getRelevantStackCallInfo() {
+ if (mIsStackCallResolved && mResolvedStackCall != null) {
+ Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
+ Iterator<Long> addrIterator = mStackCallAddresses.iterator();
+
+ while (sourceIterator.hasNext() && addrIterator.hasNext()) {
+ long addr = addrIterator.next();
+ NativeStackCallInfo info = sourceIterator.next();
+ if (addr != 0 && info != null) {
+ if (isRelevant(info.getMethodName())) {
+ return info;
+ }
+ }
+ }
+
+ // couldnt find a relevant one, so we'll return the first one if it
+ // exists.
+ if (mResolvedStackCall.size() > 0)
+ return mResolvedStackCall.get(0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if the method name is relevant.
+ * @param methodName the method name to test.
+ */
+ private boolean isRelevant(String methodName) {
+ for (String filter : sAllocFunctionFilter) {
+ if (methodName.contains(filter)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java
new file mode 100644
index 0000000..5a26317
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java
new file mode 100644
index 0000000..e54818d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java
@@ -0,0 +1,95 @@
+/*
+ * 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 final static Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$");
+
+ /** 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>
+ * &lt;sourcefile&gt;:&lt;linenumber&gt;
+ */
+ private String mSourceFile;
+
+ private int mLineNumber = -1;
+
+ /**
+ * Basic constructor with library, method, and sourcefile information
+ *
+ * @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(String lib, String method, String sourceFile) {
+ 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 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;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java
new file mode 100644
index 0000000..d2b5a1e
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.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.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)
+ */
+ public void addOutput(byte[] data, int offset, int length) {
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
+ */
+ public void flush() {
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled()
+ */
+ public boolean isCancelled() {
+ return false;
+ }
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java
new file mode 100644
index 0000000..610cb59
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Data representing an image taken from a device frame buffer.
+ */
+public final class RawImage {
+ /**
+ * bit-per-pixel value.
+ */
+ public int bpp;
+ public int size;
+ public int width;
+ public int height;
+
+ public byte[] data;
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java
new file mode 100644
index 0000000..44df000
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java
@@ -0,0 +1,949 @@
+/*
+ * 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.utils.ArrayHelper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+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 final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' };
+ private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' };
+ private final static byte[] ID_STAT = { 'S', 'T', 'A', 'T' };
+ private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' };
+ private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' };
+ private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' };
+ private final static 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 final static NullSyncProgresMonitor sNullSyncProgressMonitor =
+ new NullSyncProgresMonitor();
+
+ private final static int S_ISOCK = 0xC000; // type: symbolic link
+ private final static int S_IFLNK = 0xA000; // type: symbolic link
+ private final static int S_IFREG = 0x8000; // type: regular file
+ private final static int S_IFBLK = 0x6000; // type: block device
+ private final static int S_IFDIR = 0x4000; // type: directory
+ private final static int S_IFCHR = 0x2000; // type: character device
+ private final static 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 final static int SYNC_DATA_MAX = 64*1024;
+ private final static int REMOTE_PATH_MAX_LENGTH = 1024;
+
+ /** Result code for transfer success. */
+ public static final int RESULT_OK = 0;
+ /** Result code for canceled transfer */
+ public static final int RESULT_CANCELED = 1;
+ /** Result code for unknown error */
+ public static final int RESULT_UNKNOWN_ERROR = 2;
+ /** Result code for network connection error */
+ public static final int RESULT_CONNECTION_ERROR = 3;
+ /** Result code for unknown remote object during a pull */
+ public static final int RESULT_NO_REMOTE_OBJECT = 4;
+ /** Result code when attempting to pull multiple files into a file */
+ public static final int RESULT_TARGET_IS_FILE = 5;
+ /** Result code when attempting to pull multiple into a directory that does not exist. */
+ public static final int RESULT_NO_DIR_TARGET = 6;
+ /** Result code for wrong encoding on the remote path. */
+ public static final int RESULT_REMOTE_PATH_ENCODING = 7;
+ /** Result code for remote path that is too long. */
+ public static final int RESULT_REMOTE_PATH_LENGTH = 8;
+ /** Result code for error while writing local file. */
+ public static final int RESULT_FILE_WRITE_ERROR = 9;
+ /** Result code for error while reading local file. */
+ public static final int RESULT_FILE_READ_ERROR = 10;
+ /** Result code for attempting to push a file that does not exist. */
+ public static final int RESULT_NO_LOCAL_FILE = 11;
+ /** Result code for attempting to push a directory. */
+ public static final int RESULT_LOCAL_IS_DIRECTORY = 12;
+ /** Result code for when the target path of a multi file push is a file. */
+ public static final int RESULT_REMOTE_IS_FILE = 13;
+ /** Result code for receiving too much data from the remove device at once */
+ public static final int RESULT_BUFFER_OVERRUN = 14;
+
+ /**
+ * A file transfer result.
+ * <p/>
+ * This contains a code, and an optional string
+ */
+ public static class SyncResult {
+ private int mCode;
+ private String mMessage;
+ SyncResult(int code, String message) {
+ mCode = code;
+ mMessage = message;
+ }
+
+ SyncResult(int code, Exception e) {
+ this(code, e.getMessage());
+ }
+
+ SyncResult(int code) {
+ this(code, errorCodeToString(code));
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+ }
+
+ /**
+ * 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 NullSyncProgresMonitor implements ISyncProgressMonitor {
+ public void advance(int work) {
+ }
+ public boolean isCanceled() {
+ return false;
+ }
+
+ public void start(int totalWork) {
+ }
+ public void startSubTask(String name) {
+ }
+ 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 otherwise.
+ */
+ boolean openSync() {
+ 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, AdbHelper.STD_TIMEOUT);
+
+ AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */);
+
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.w("ddms",
+ "Got timeout or unhappy response from ADB sync req: "
+ + resp.message);
+ mChannel.close();
+ mChannel = null;
+ return false;
+ }
+ } catch (IOException e) {
+ if (mChannel != null) {
+ try {
+ mChannel.close();
+ } catch (IOException e1) {
+ // we do nothing, since we'll return false just below
+ }
+ mChannel = null;
+ return false;
+ }
+ }
+ 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;
+ }
+
+ /**
+ * Converts an error code into a non-localized string
+ * @param code the error code;
+ */
+ private static String errorCodeToString(int code) {
+ switch (code) {
+ case RESULT_OK:
+ return "Success.";
+ case RESULT_CANCELED:
+ return "Tranfert canceled by the user.";
+ case RESULT_UNKNOWN_ERROR:
+ return "Unknown Error.";
+ case RESULT_CONNECTION_ERROR:
+ return "Adb Connection Error.";
+ case RESULT_NO_REMOTE_OBJECT:
+ return "Remote object doesn't exist!";
+ case RESULT_TARGET_IS_FILE:
+ return "Target object is a file.";
+ case RESULT_NO_DIR_TARGET:
+ return "Target directory doesn't exist.";
+ case RESULT_REMOTE_PATH_ENCODING:
+ return "Remote Path encoding is not supported.";
+ case RESULT_REMOTE_PATH_LENGTH:
+ return "Remove path is too long.";
+ case RESULT_FILE_WRITE_ERROR:
+ return "Writing local file failed!";
+ case RESULT_FILE_READ_ERROR:
+ return "Reading local file failed!";
+ case RESULT_NO_LOCAL_FILE:
+ return "Local file doesn't exist.";
+ case RESULT_LOCAL_IS_DIRECTORY:
+ return "Local path is a directory.";
+ case RESULT_REMOTE_IS_FILE:
+ return "Remote path is a file.";
+ case RESULT_BUFFER_OVERRUN:
+ return "Receiving too much data.";
+ }
+
+ throw new RuntimeException();
+ }
+
+ /**
+ * 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.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ *
+ * @see FileListingService.FileEntry
+ * @see #getNullProgressMonitor()
+ */
+ public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) {
+
+ // first we check the destination is a directory and exists
+ File f = new File(localPath);
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_DIR_TARGET);
+ }
+ if (f.isDirectory() == false) {
+ return new SyncResult(RESULT_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);
+
+ SyncResult result = doPull(entries, localPath, fls, monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * Pulls a single file.
+ * @param remote the remote file
+ * @param localFilename The local destination.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ *
+ * @see FileListingService.FileEntry
+ * @see #getNullProgressMonitor()
+ */
+ public SyncResult pullFile(FileEntry remote, String localFilename,
+ ISyncProgressMonitor monitor) {
+ int total = remote.getSizeValue();
+ monitor.start(total);
+
+ SyncResult result = doPullFile(remote.getFullPath(), localFilename, monitor);
+
+ monitor.stop();
+ return result;
+ }
+
+ /**
+ * 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.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) {
+ if (remote.isDirectory() == false) {
+ return new SyncResult(RESULT_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);
+
+ SyncResult result = doPush(fileArray, remote.getFullPath(), monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * Push a single file.
+ * @param local the local filepath.
+ * @param remote The remote filepath.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) {
+ File f = new File(local);
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_LOCAL_FILE);
+ }
+
+ if (f.isDirectory()) {
+ return new SyncResult(RESULT_LOCAL_IS_DIRECTORY);
+ }
+
+ monitor.start((int)f.length());
+
+ SyncResult result = doPushFile(local, remote, monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * 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.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPull(FileEntry[] entries, String localPath,
+ FileListingService fileListingService,
+ ISyncProgressMonitor monitor) {
+
+ for (FileEntry e : entries) {
+ // check if we're cancelled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_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);
+ SyncResult result = doPull(children, dest, fileListingService, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ monitor.advance(1);
+ } else if (type == FileListingService.TYPE_FILE) {
+ monitor.startSubTask(e.getFullPath());
+ String dest = localPath + File.separator + e.getName();
+ SyncResult result = doPullFile(e.getFullPath(), dest, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ }
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+ /**
+ * 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.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPullFile(String remotePath, String localPath,
+ ISyncProgressMonitor monitor) {
+ byte[] msg = null;
+ byte[] pullResult = new byte[8];
+ try {
+ byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+ if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+ return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
+ }
+
+ // create the full request message
+ msg = createFileReq(ID_RECV, remotePathContent);
+
+ // and send it.
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
+
+ // read the result, in a byte array containing 2 ints
+ // (id, size)
+ AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
+
+ // check we have the proper data back
+ if (checkResult(pullResult, ID_DATA) == false &&
+ checkResult(pullResult, ID_DONE) == false) {
+ return new SyncResult(RESULT_CONNECTION_ERROR);
+ }
+ } catch (UnsupportedEncodingException e) {
+ return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, 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);
+ } catch (FileNotFoundException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+
+ // 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() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+
+ // if we're done, we stop the loop
+ if (checkResult(pullResult, ID_DONE)) {
+ break;
+ }
+ if (checkResult(pullResult, ID_DATA) == false) {
+ // hmm there's an error
+ return new SyncResult(RESULT_CONNECTION_ERROR);
+ }
+ int length = ArrayHelper.swap32bitFromArray(pullResult, 4);
+ if (length > SYNC_DATA_MAX) {
+ // buffer overrun!
+ // error and exit
+ return new SyncResult(RESULT_BUFFER_OVERRUN);
+ }
+
+ try {
+ // now read the length we received
+ AdbHelper.read(mChannel, data, length, AdbHelper.STD_TIMEOUT);
+
+ // get the header for the next packet.
+ AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // write the content in the file
+ try {
+ fos.write(data, 0, length);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+
+ monitor.advance(length);
+ }
+
+ try {
+ fos.flush();
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+ return new SyncResult(RESULT_OK);
+ }
+
+
+ /**
+ * Push multiple files
+ * @param fileArray
+ * @param remotePath
+ * @param monitor
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) {
+ for (File f : fileArray) {
+ // check if we're canceled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_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);
+ SyncResult result = doPush(f.listFiles(), dest, monitor);
+
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+
+ 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);
+ SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+ /**
+ * 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.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPushFile(String localPath, String remotePath,
+ ISyncProgressMonitor monitor) {
+ FileInputStream fis = null;
+ byte[] msg;
+
+ try {
+ byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+ if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+ return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
+ }
+
+ File f = new File(localPath);
+
+ // this shouldn't happen but still...
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_LOCAL_FILE);
+ }
+
+ // create the stream to read the file
+ fis = new FileInputStream(f);
+
+ // create the header for the action
+ msg = createSendFileReq(ID_SEND, remotePathContent, 0644);
+ } catch (UnsupportedEncodingException e) {
+ return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
+ } catch (FileNotFoundException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ // and send it. We use a custom try/catch block to make the difference between
+ // file and network IO exceptions.
+ try {
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // create the buffer used to read.
+ // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning.
+ if (mBuffer == null) {
+ mBuffer = new byte[SYNC_DATA_MAX + 8];
+ }
+ System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length);
+
+ // look while there is something to read
+ while (true) {
+ // check if we're canceled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+
+ // read up to SYNC_DATA_MAX
+ int readCount = 0;
+ try {
+ readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ 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, mBuffer, 4);
+
+ // now write it
+ try {
+ AdbHelper.write(mChannel, mBuffer, readCount+8, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // and advance the monitor
+ monitor.advance(readCount);
+ }
+ // close the local file
+ try {
+ fis.close();
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ try {
+ // create the DONE message
+ long time = System.currentTimeMillis() / 1000;
+ msg = createReq(ID_DONE, (int)time);
+
+ // and send it.
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_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 */, AdbHelper.STD_TIMEOUT);
+
+ if (checkResult(result, ID_OKAY) == false) {
+ if (checkResult(result, ID_FAIL)) {
+ // read some error message...
+ int len = ArrayHelper.swap32bitFromArray(result, 4);
+
+ AdbHelper.read(mChannel, mBuffer, len, AdbHelper.STD_TIMEOUT);
+
+ // output the result?
+ String message = new String(mBuffer, 0, len);
+ Log.e("ddms", "transfer error: " + message);
+ return new SyncResult(RESULT_UNKNOWN_ERROR, message);
+ }
+
+ return new SyncResult(RESULT_UNKNOWN_ERROR);
+ }
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+
+ /**
+ * Returns the mode of the remote file.
+ * @param path the remote file
+ * @return and Integer containing the mode if all went well or null
+ * otherwise
+ */
+ private Integer readMode(String path) {
+ try {
+ // create the stat request message.
+ byte[] msg = createFileReq(ID_STAT, path);
+
+ AdbHelper.write(mChannel, msg, -1 /* full length */, AdbHelper.STD_TIMEOUT);
+
+ // 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 */, AdbHelper.STD_TIMEOUT);
+
+ // check we have the proper data back
+ if (checkResult(statResult, ID_STAT) == false) {
+ return null;
+ }
+
+ // we return the mode (2nd int in the array)
+ return ArrayHelper.swap32bitFromArray(statResult, 4);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 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) {
+ if (result[0] != code[0] ||
+ result[1] != code[1] ||
+ result[2] != code[2] ||
+ result[3] != code[3]) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+ 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;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java
new file mode 100644
index 0000000..8f284f3
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java
@@ -0,0 +1,139 @@
+/*
+ * 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()
+ */
+ public StackTraceElement[] getStackTrace() {
+ return mTrace;
+ }
+
+ /**
+ * Returns the approximate time of the stacktrace data.
+ * @see #getStackTrace()
+ */
+ public long getStackCallTime() {
+ return mTraceTime;
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java
new file mode 100644
index 0000000..ec9186c
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java
@@ -0,0 +1,461 @@
+/*
+ * 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.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 final static 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 + "@" + (String)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();
+ }
+
+ 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()) == false) {
+ 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).intValue() & ((Integer)value).intValue()) != 0;
+ } else if (data instanceof Long) {
+ return (((Long)data).longValue() & ((Long)value).longValue()) != 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 ((Integer)data).toString();
+ case LONG:
+ return ((Long)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/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java
new file mode 100644
index 0000000..85e99c1
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java
@@ -0,0 +1,577 @@
+/*
+ * 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.Device;
+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.Set;
+import java.util.TreeMap;
+import java.util.Map.Entry;
+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 final static 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 final static int EVENT_TYPE_INT = 0;
+ private final static int EVENT_TYPE_LONG = 1;
+ private final static int EVENT_TYPE_STRING = 2;
+ private final static int EVENT_TYPE_LIST = 3;
+
+ private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
+ "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
+ private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
+ "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
+ private final static Pattern PATTERN_DESCRIPTION = Pattern.compile(
+ "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$
+
+ private final static 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(Device 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);
+ }
+ }
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ 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) {
+ try {
+ BufferedReader 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;
+ }
+ }
+
+ /**
+ * 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.length() > 0 && 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.length() > 0) {
+ 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.length() > 0) {
+ //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.size() == 0) {
+ 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.length() == 0) {
+ 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(new Integer(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(new Long(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/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java
new file mode 100644
index 0000000..b68b4e8
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java
@@ -0,0 +1,214 @@
+/*
+ * 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;
+
+
+/**
+ * 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();
+ }
+
+ 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/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java
new file mode 100644
index 0000000..7bae202
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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 final static 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) == false) {
+ throw new InvalidTypeException();
+ }
+ } else if ((value instanceof Long) == false) {
+ throw new InvalidTypeException();
+ }
+
+ switch (compareMethod) {
+ case EQUAL_TO:
+ if (index == 0) {
+ return processId.equals(value);
+ } else {
+ return getValueAsLong(index) == ((Long)value).longValue();
+ }
+ case LESSER_THAN:
+ return getValueAsLong(index) <= ((Long)value).longValue();
+ case LESSER_THAN_STRICT:
+ return getValueAsLong(index) < ((Long)value).longValue();
+ case GREATER_THAN:
+ return getValueAsLong(index) >= ((Long)value).longValue();
+ case GREATER_THAN_STRICT:
+ return getValueAsLong(index) > ((Long)value).longValue();
+ case BIT_CHECK:
+ return (getValueAsLong(index) & ((Long)value).longValue()) != 0;
+ }
+
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ @Override
+ public Object getValue(int valueIndex) {
+ if (valueIndex == 0) {
+ return processId;
+ }
+
+ try {
+ return new Long(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/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java
new file mode 100644
index 0000000..016f8aa
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java
new file mode 100644
index 0000000..a3050c8
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java
new file mode 100644
index 0000000..b49f025
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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 final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry.
+
+ /**
+ * Represents a log entry and its raw data.
+ */
+ public final static 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 == false) {
+ // 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/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java
new file mode 100644
index 0000000..b61a698
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+/**
+ * Receives event notifications during instrumentation test runs.
+ * Patterned after {@link junit.runner.TestRunListener}.
+ */
+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 testCount total number of tests in test run
+ */
+ public void testRunStarted(int testCount);
+
+ /**
+ * Reports end of test run.
+ *
+ * @param elapsedTime device reported elapsed time, in milliseconds
+ */
+ public void testRunEnded(long elapsedTime);
+
+ /**
+ * Reports test run stopped before completion.
+ *
+ * @param elapsedTime device reported elapsed time, in milliseconds
+ */
+ public void testRunStopped(long elapsedTime);
+
+ /**
+ * Reports the start of an individual test case.
+ *
+ * @param test identifies the test
+ */
+ public void testStarted(TestIdentifier test);
+
+ /**
+ * Reports the execution end of an individual test case.
+ * If {@link #testFailed} was not invoked, this test passed.
+ *
+ * @param test identifies the test
+ */
+ public void testEnded(TestIdentifier test);
+
+ /**
+ * Reports the failure of a individual test case.
+ * 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 test run failed to execute due to a fatal error.
+ */
+ public void testRunFailed(String errorMessage);
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
new file mode 100755
index 0000000..bc1834f
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.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.ddmlib.testrunner;
+
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+
+/**
+ * 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_FAILED: </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";
+ }
+
+ /** 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;
+ }
+
+ /** 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 TIME_REPORT = "Time: ";
+ }
+
+ private final ITestRunListener mTestListener;
+
+ /**
+ * 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;
+ }
+ }
+
+ /** Stores the status values for the test result currently being parsed */
+ private TestResult mCurrentTestResult = 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;
+
+ /** 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;
+
+ private static final String LOG_TAG = "InstrumentationResultParser";
+
+ /**
+ * Creates the InstrumentationResultParser.
+ *
+ * @param listener informed of test results as the tests are executing
+ */
+ public InstrumentationResultParser(ITestRunListener listener) {
+ mTestListener = listener;
+ }
+
+ /**
+ * Processes the instrumentation test output from shell.
+ *
+ * @see MultiLineReceiver#processNewLines
+ */
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ parse(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();
+ parseStatusCode(line);
+ } else if (line.startsWith(Prefixes.STATUS)) {
+ // Previous status key-value has been collected. Store it.
+ submitCurrentKeyValue();
+ parseKey(line, Prefixes.STATUS.length());
+ } else if (line.startsWith(Prefixes.STATUS_FAILED)) {
+ Log.e(LOG_TAG, "test run failed " + line);
+ mTestListener.testRunFailed(line);
+ } else if (line.startsWith(Prefixes.TIME_REPORT)) {
+ parseTime(line, Prefixes.TIME_REPORT.length());
+ } else {
+ if (mCurrentValue != null) {
+ // this is a value that has wrapped to next line.
+ mCurrentValue.append("\r\n");
+ mCurrentValue.append(line);
+ } else {
+ Log.w(LOG_TAG, "unrecognized line " + line);
+ }
+ }
+ }
+
+ /**
+ * Stores the currently parsed key-value pair into mCurrentTestInfo.
+ */
+ private void submitCurrentKeyValue() {
+ if (mCurrentKey != null && mCurrentValue != null) {
+ TestResult testInfo = getCurrentTestInfo();
+ String statusValue = mCurrentValue.toString();
+
+ 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.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue);
+ }
+ }
+ else if (mCurrentKey.equals(StatusKeys.STACK)) {
+ testInfo.mStackTrace = statusValue;
+ }
+
+ mCurrentKey = null;
+ mCurrentValue = null;
+ }
+ }
+
+ private TestResult getCurrentTestInfo() {
+ if (mCurrentTestResult == null) {
+ mCurrentTestResult = new TestResult();
+ }
+ return mCurrentTestResult;
+ }
+
+ private void clearCurrentTestInfo() {
+ 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();
+ try {
+ testInfo.mCode = Integer.parseInt(value);
+ }
+ catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Expected integer status code, received: " + value);
+ }
+
+ // this means we're done with current test result bundle
+ reportResult(testInfo);
+ clearCurrentTestInfo();
+ }
+
+ /**
+ * Returns true if test run canceled.
+ *
+ * @see IShellOutputReceiver#isCancelled()
+ */
+ 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.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
+ return;
+ }
+ reportTestRunStarted(testInfo);
+ TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
+
+ switch (testInfo.mCode) {
+ case StatusCodes.START:
+ mTestListener.testStarted(testId);
+ break;
+ case StatusCodes.FAILURE:
+ mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
+ getTrace(testInfo));
+ mTestListener.testEnded(testId);
+ break;
+ case StatusCodes.ERROR:
+ mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+ getTrace(testInfo));
+ mTestListener.testEnded(testId);
+ break;
+ case StatusCodes.OK:
+ mTestListener.testEnded(testId);
+ break;
+ default:
+ Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
+ mTestListener.testEnded(testId);
+ 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) {
+ mTestListener.testRunStarted(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, int startPos) {
+ String timeString = line.substring(startPos);
+ try {
+ float timeSeconds = Float.parseFloat(timeString);
+ mTestTime = (long)(timeSeconds * 1000);
+ }
+ catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Unexpected time format " + timeString);
+ }
+ }
+
+ /**
+ * Called by parent when adb session is complete.
+ */
+ @Override
+ public void done() {
+ super.done();
+ mTestListener.testRunEnded(mTestTime);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
new file mode 100644
index 0000000..4edbbbb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
@@ -0,0 +1,228 @@
+/*
+ * 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.IDevice;
+import com.android.ddmlib.Log;
+
+import java.io.IOException;
+
+/**
+ * Runs a Android test command remotely and reports results.
+ */
+public class RemoteAndroidTestRunner {
+
+ private static final char CLASS_SEPARATOR = ',';
+ private static final char METHOD_SEPARATOR = '#';
+ private static final char RUNNER_SEPARATOR = '/';
+ private String mClassArg;
+ private final String mPackageName;
+ private final String mRunnerName;
+ private String mExtraArgs;
+ private boolean mLogOnlyMode;
+ private IDevice mRemoteDevice;
+ private InstrumentationResultParser mParser;
+
+ private static final String LOG_TAG = "RemoteAndroidTest";
+ private static final String DEFAULT_RUNNER_NAME =
+ "android.test.InstrumentationTestRunner";
+
+ /**
+ * 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;
+ mClassArg = null;
+ mExtraArgs = "";
+ mLogOnlyMode = false;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Returns the application package name.
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the runnerName.
+ */
+ 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();
+ }
+
+ /**
+ * 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) {
+ mClassArg = 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) {
+ StringBuilder classArgBuilder = new StringBuilder();
+
+ for (int i=0; i < classNames.length; i++) {
+ if (i != 0) {
+ classArgBuilder.append(CLASS_SEPARATOR);
+ }
+ classArgBuilder.append(classNames[i]);
+ }
+ mClassArg = classArgBuilder.toString();
+ }
+
+ /**
+ * 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) {
+ mClassArg = className + METHOD_SEPARATOR + testName;
+ }
+
+ /**
+ * Sets extra arguments to include in instrumentation command.
+ * Must be called before 'run'.
+ *
+ * @param instrumentationArgs must not be null
+ */
+ public void setExtraArgs(String instrumentationArgs) {
+ if (instrumentationArgs == null) {
+ throw new IllegalArgumentException("instrumentationArgs cannot be null");
+ }
+ mExtraArgs = instrumentationArgs;
+ }
+
+ /**
+ * Returns the extra instrumentation arguments.
+ */
+ public String getExtraArgs() {
+ return mExtraArgs;
+ }
+
+ /**
+ * Sets this test run to log only mode - skips test execution.
+ */
+ public void setLogOnly(boolean logOnly) {
+ mLogOnlyMode = logOnly;
+ }
+
+ /**
+ * Execute this test run.
+ *
+ * @param listener listens for test results
+ */
+ public void run(ITestRunListener listener) {
+ final String runCaseCommandStr = "am instrument -w -r "
+ + getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath();
+ Log.d(LOG_TAG, runCaseCommandStr);
+ mParser = new InstrumentationResultParser(listener);
+
+ try {
+ mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e);
+ listener.testRunFailed(e.toString());
+ }
+ }
+
+ /**
+ * Requests cancellation of this test run.
+ */
+ public void cancel() {
+ if (mParser != null) {
+ mParser.cancel();
+ }
+ }
+
+ /**
+ * Returns the test class argument.
+ */
+ private String getClassArg() {
+ return mClassArg;
+ }
+
+ /**
+ * Returns the full instrumentation command which specifies the test classes to execute.
+ * Returns an empty string if no classes were specified.
+ */
+ private String getClassCmd() {
+ String classArg = getClassArg();
+ if (classArg != null) {
+ return "-e class " + classArg;
+ }
+ return "";
+ }
+
+ /**
+ * Returns the full command to enable log only mode - if specified. Otherwise returns an
+ * empty string.
+ */
+ private String getLogCmd() {
+ if (mLogOnlyMode) {
+ return "-e log true";
+ }
+ else {
+ return "";
+ }
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java
new file mode 100644
index 0000000..4d3b108
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+ }
+
+ /**
+ * Tests equality by comparing class and method name
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof TestIdentifier)) {
+ return false;
+ }
+ TestIdentifier otherTest = (TestIdentifier)other;
+ return getClassName().equals(otherTest.getClassName()) &&
+ getTestName().equals(otherTest.getTestName());
+ }
+
+ /**
+ * Generates hashCode based on class and method name.
+ */
+ @Override
+ public int hashCode() {
+ return getClassName().hashCode() * 31 + getTestName().hashCode();
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java
new file mode 100644
index 0000000..8167e5d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/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/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
new file mode 100644
index 0000000..77d10c1
--- /dev/null
+++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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 junit.framework.TestCase;
+
+
+/**
+ * Tests InstrumentationResultParser.
+ */
+public class InstrumentationResultParserTest extends TestCase {
+
+ private InstrumentationResultParser mParser;
+ private VerifyingTestResult mTestResult;
+
+ // static dummy test names to use for validation
+ private static final String CLASS_NAME = "com.test.FooTest";
+ private static final String TEST_NAME = "testFoo";
+ private static final String STACK_TRACE = "java.lang.AssertionFailedException";
+
+ /**
+ * @param name - test name
+ */
+ public InstrumentationResultParserTest(String name) {
+ super(name);
+ }
+
+ /**
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTestResult = new VerifyingTestResult();
+ mParser = new InstrumentationResultParser(mTestResult);
+ }
+
+ /**
+ * Tests that the test run started and test start events is sent on first
+ * bundle received.
+ */
+ public void testTestStarted() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+ assertEquals(0, mTestResult.mNumTestsRun);
+ }
+
+ /**
+ * Tests that a single successful test execution.
+ */
+ public void testTestSuccess() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+ addCommonStatus(output);
+ addSuccessCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+ assertEquals(1, mTestResult.mNumTestsRun);
+ assertEquals(null, mTestResult.mTestStatus);
+ }
+
+ /**
+ * Test basic parsing of failed test case.
+ */
+ public void testTestFailed() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+ addCommonStatus(output);
+ addStackTrace(output);
+ addFailureCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+
+ assertEquals(1, mTestResult.mNumTestsRun);
+ assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus);
+ assertEquals(STACK_TRACE, mTestResult.mTrace);
+ }
+
+ /**
+ * Test basic parsing and conversion of time from output.
+ */
+ public void testTimeParsing() {
+ final String timeString = "Time: 4.9";
+ injectTestString(timeString);
+ assertEquals(4900, mTestResult.mTestTime);
+ }
+
+ /**
+ * builds a common test result using TEST_NAME and TEST_CLASS.
+ */
+ private StringBuilder buildCommonResult() {
+ StringBuilder output = new StringBuilder();
+ // add test start bundle
+ addCommonStatus(output);
+ addStatusCode(output, "1");
+ // add end test bundle, without status
+ addCommonStatus(output);
+ return output;
+ }
+
+ /**
+ * Adds common status results to the provided output.
+ */
+ private void addCommonStatus(StringBuilder output) {
+ addStatusKey(output, "stream", "\r\n" + CLASS_NAME);
+ addStatusKey(output, "test", TEST_NAME);
+ addStatusKey(output, "class", CLASS_NAME);
+ addStatusKey(output, "current", "1");
+ addStatusKey(output, "numtests", "1");
+ addStatusKey(output, "id", "InstrumentationTestRunner");
+ }
+
+ /**
+ * Adds a stack trace status bundle to output.
+ */
+ private void addStackTrace(StringBuilder output) {
+ addStatusKey(output, "stack", STACK_TRACE);
+
+ }
+
+ /**
+ * Helper method to add a status key-value bundle.
+ */
+ private void addStatusKey(StringBuilder outputBuilder, String key,
+ String value) {
+ outputBuilder.append("INSTRUMENTATION_STATUS: ");
+ outputBuilder.append(key);
+ outputBuilder.append('=');
+ outputBuilder.append(value);
+ outputBuilder.append("\r\n");
+ }
+
+ private void addStartCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "1");
+ }
+
+ private void addSuccessCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "0");
+ }
+
+ private void addFailureCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "-2");
+ }
+
+ private void addStatusCode(StringBuilder outputBuilder, String value) {
+ outputBuilder.append("INSTRUMENTATION_STATUS_CODE: ");
+ outputBuilder.append(value);
+ outputBuilder.append("\r\n");
+ }
+
+ /**
+ * inject a test string into the result parser.
+ *
+ * @param result
+ */
+ private void injectTestString(String result) {
+ byte[] data = result.getBytes();
+ mParser.addOutput(data, 0, data.length);
+ mParser.flush();
+ }
+
+ private void assertCommonAttributes() {
+ assertEquals(CLASS_NAME, mTestResult.mSuiteName);
+ assertEquals(1, mTestResult.mTestCount);
+ assertEquals(TEST_NAME, mTestResult.mTestName);
+ }
+
+ /**
+ * A specialized test listener that stores a single test events.
+ */
+ private class VerifyingTestResult implements ITestRunListener {
+
+ String mSuiteName;
+ int mTestCount;
+ int mNumTestsRun;
+ String mTestName;
+ long mTestTime;
+ TestFailure mTestStatus;
+ String mTrace;
+ boolean mStopped;
+
+ VerifyingTestResult() {
+ mNumTestsRun = 0;
+ mTestStatus = null;
+ mStopped = false;
+ }
+
+ public void testEnded(TestIdentifier test) {
+ mNumTestsRun++;
+ assertEquals("Unexpected class name", mSuiteName, test.getClassName());
+ assertEquals("Unexpected test ended", mTestName, test.getTestName());
+
+ }
+
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ mTestStatus = status;
+ mTrace = trace;
+ assertEquals("Unexpected class name", mSuiteName, test.getClassName());
+ assertEquals("Unexpected test ended", mTestName, test.getTestName());
+ }
+
+ public void testRunEnded(long elapsedTime) {
+ mTestTime = elapsedTime;
+
+ }
+
+ public void testRunStarted(int testCount) {
+ mTestCount = testCount;
+ }
+
+ public void testRunStopped(long elapsedTime) {
+ mTestTime = elapsedTime;
+ mStopped = true;
+ }
+
+ public void testStarted(TestIdentifier test) {
+ mSuiteName = test.getClassName();
+ mTestName = test.getTestName();
+ }
+
+ public void testRunFailed(String errorMessage) {
+ // ignored
+ }
+ }
+
+}
diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
new file mode 100644
index 0000000..9acaaf9
--- /dev/null
+++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.Client;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.Device.DeviceState;
+import com.android.ddmlib.log.LogReceiver;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Tests RemoteAndroidTestRunner.
+ */
+public class RemoteAndroidTestRunnerTest extends TestCase {
+
+ private RemoteAndroidTestRunner mRunner;
+ private MockDevice mMockDevice;
+
+ private static final String TEST_PACKAGE = "com.test";
+ private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner";
+
+ /**
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ mMockDevice = new MockDevice();
+ mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice);
+ }
+
+ /**
+ * Test the basic case building of the instrumentation runner command with no arguments.
+ */
+ public void testRun() {
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER),
+ mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with log set.
+ */
+ public void testRunWithLog() {
+ mRunner.setLogOnly(true);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE,
+ TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with method set.
+ */
+ public void testRunWithMethod() {
+ final String className = "FooTest";
+ final String testName = "fooTest";
+ mRunner.setMethodName(className, testName);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className,
+ testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with extra args set.
+ */
+ public void testRunWithExtraArgs() {
+ final String extraArgs = "blah";
+ mRunner.setExtraArgs(extraArgs);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs,
+ TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+
+ /**
+ * Assert two strings are equal ignoring whitespace.
+ */
+ private void assertStringsEquals(String str1, String str2) {
+ String strippedStr1 = str1.replaceAll(" ", "");
+ String strippedStr2 = str2.replaceAll(" ", "");
+ assertEquals(strippedStr1, strippedStr2);
+ }
+
+ /**
+ * A dummy device that does nothing except store the provided executed shell command for
+ * later retrieval.
+ */
+ private static class MockDevice implements IDevice {
+
+ private String mLastShellCommand;
+
+ /**
+ * Stores the provided command for later retrieval from getLastShellCommand.
+ */
+ public void executeShellCommand(String command,
+ IShellOutputReceiver receiver) throws IOException {
+ mLastShellCommand = command;
+ }
+
+ /**
+ * Get the last command provided to executeShellCommand.
+ */
+ public String getLastShellCommand() {
+ return mLastShellCommand;
+ }
+
+ public boolean createForward(int localPort, int remotePort) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Client getClient(String applicationName) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getClientName(int pid) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Client[] getClients() {
+ throw new UnsupportedOperationException();
+ }
+
+ public FileListingService getFileListingService() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Map<String, String> getProperties() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getProperty(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getPropertyCount() {
+ throw new UnsupportedOperationException();
+ }
+
+ public RawImage getScreenshot() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getSerialNumber() {
+ throw new UnsupportedOperationException();
+ }
+
+ public DeviceState getState() {
+ throw new UnsupportedOperationException();
+ }
+
+ public SyncService getSyncService() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean hasClients() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isBootLoader() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isEmulator() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isOffline() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isOnline() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean removeForward(int localPort, int remotePort) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void runEventLogService(LogReceiver receiver) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public void runLogService(String logname, LogReceiver receiver) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getAvdName() {
+ return "";
+ }
+
+ }
+
+ /**
+ * An empty implementation of ITestRunListener.
+ */
+ private static class EmptyListener implements ITestRunListener {
+
+ public void testEnded(TestIdentifier test) {
+ // ignore
+ }
+
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ // ignore
+ }
+
+ public void testRunEnded(long elapsedTime) {
+ // ignore
+ }
+
+ public void testRunFailed(String errorMessage) {
+ // ignore
+ }
+
+ public void testRunStarted(int testCount) {
+ // ignore
+ }
+
+ public void testRunStopped(long elapsedTime) {
+ // ignore
+ }
+
+ public void testStarted(TestIdentifier test) {
+ // ignore
+ }
+
+ }
+}
diff --git a/ddms/libs/ddmuilib/.classpath b/ddms/libs/ddmuilib/.classpath
new file mode 100644
index 0000000..ce7e7f0
--- /dev/null
+++ b/ddms/libs/ddmuilib/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry excluding="Makefile|resources" kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_JFREECHART"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/libs/ddmuilib/.project b/ddms/libs/ddmuilib/.project
new file mode 100644
index 0000000..29cb2f2
--- /dev/null
+++ b/ddms/libs/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/libs/ddmuilib/Android.mk b/ddms/libs/ddmuilib/Android.mk
new file mode 100644
index 0000000..7059e5e
--- /dev/null
+++ b/ddms/libs/ddmuilib/Android.mk
@@ -0,0 +1,4 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMUILIB_LOCAL_DIR := $(call my-dir)
+include $(DDMUILIB_LOCAL_DIR)/src/Android.mk
diff --git a/ddms/libs/ddmuilib/README b/ddms/libs/ddmuilib/README
new file mode 100644
index 0000000..d66b84a
--- /dev/null
+++ b/ddms/libs/ddmuilib/README
@@ -0,0 +1,11 @@
+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
+available at prebuild/<platform>/swt. \ No newline at end of file
diff --git a/ddms/libs/ddmuilib/src/Android.mk b/ddms/libs/ddmuilib/src/Android.mk
new file mode 100644
index 0000000..acbda44
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/Android.mk
@@ -0,0 +1,22 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_RESOURCE_DIRS := resources
+
+LOCAL_JAVA_LIBRARIES := \
+ ddmlib \
+ swt \
+ org.eclipse.jface_3.2.0.I20060605-1400 \
+ org.eclipse.equinox.common_3.2.0.v20060603 \
+ org.eclipse.core.commands_3.2.0.I20060605-1400 \
+ jcommon-1.0.12 \
+ jfreechart-1.0.9 \
+ jfreechart-1.0.9-swt
+
+LOCAL_MODULE := ddmuilib
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java
new file mode 100644
index 0000000..a2f12d5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java
@@ -0,0 +1,278 @@
+/*
+ * 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.*;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * 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 {
+
+ /**
+ * 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 String 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;
+
+ /**
+ * 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 String library) {
+ // synchronize around the hashmap object
+ if (library != null) {
+ synchronized (sProcessCache) {
+ // look for an existing process
+ Addr2Line process = sProcessCache.get(library);
+
+ // 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(library, 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.
+ * <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 address.
+ */
+ private Addr2Line(final String library) {
+ mLibrary = library;
+ }
+
+ /**
+ * 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.
+
+ // get the output directory.
+ String symbols = DdmUiPreferences.getSymbolDirectory();
+
+ // build the command line
+ String[] command = new String[5];
+ command[0] = DdmUiPreferences.getAddr2Line();
+ command[1] = "-C";
+ command[2] = "-f";
+ command[3] = "-e";
+ command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so");
+
+ 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) {
+ // 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(addr, 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(mLibrary, 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/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java
new file mode 100644
index 0000000..45d45ff
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java
@@ -0,0 +1,487 @@
+/*
+ * 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.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+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.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.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Base class for our information panels.
+ */
+public class AllocationPanel extends TablePanel {
+
+ 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_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$
+
+ private Composite mAllocationBase;
+ private Table mAllocationTable;
+ private TableViewer mAllocationViewer;
+
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+ private Button mEnableButton;
+ private Button mRequestButton;
+
+ /**
+ * 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 static class AllocationContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations();
+ if (allocs != null) {
+ return allocs;
+ }
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ 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 {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof AllocationInfo) {
+ AllocationInfo alloc = (AllocationInfo)element;
+ switch (columnIndex) {
+ case 0:
+ return Integer.toString(alloc.getSize());
+ case 1:
+ return alloc.getAllocatedClass();
+ case 2:
+ return Short.toString(alloc.getThreadId());
+ case 3:
+ StackTraceElement[] traces = alloc.getStackTrace();
+ if (traces.length > 0) {
+ return traces[0].getClassName();
+ }
+ break;
+ case 4:
+ traces = alloc.getStackTrace();
+ if (traces.length > 0) {
+ return traces[0].getMethodName();
+ }
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // 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(2, false));
+
+ mEnableButton = new Button(topParent, SWT.PUSH);
+ mEnableButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Client current = getCurrentClient();
+ int status = current.getClientData().getAllocationStatus();
+ if (status == ClientData.ALLOCATION_TRACKING_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 */, ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */);
+
+ mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION);
+ GridData gridData;
+ mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH));
+ gridData.horizontalSpan = 2;
+ mAllocationTable.setHeaderVisible(true);
+ mAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocation Size",
+ SWT.RIGHT,
+ "888", //$NON-NLS-1$
+ PREFS_ALLOC_COL_SIZE, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated Class",
+ SWT.LEFT,
+ "Allocated Class", //$NON-NLS-1$
+ PREFS_ALLOC_COL_CLASS, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Thread Id",
+ SWT.LEFT,
+ "999", //$NON-NLS-1$
+ PREFS_ALLOC_COL_THREAD, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_CLASS, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_METHOD, store);
+
+ mAllocationViewer = new TableViewer(mAllocationTable);
+ mAllocationViewer.setContentProvider(new AllocationContentProvider());
+ mAllocationViewer.setLabelProvider(new AllocationLabelProvider());
+
+ mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ 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_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ 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() {
+ 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;
+ }
+
+ /**
+ * 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_INTEREST}, {@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)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) {
+ try {
+ mAllocationTable.getDisplay().asyncExec(new Runnable() {
+ 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() {
+ 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 */,
+ ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */);
+ }
+
+ 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, int trackingStatus) {
+ if (enabled) {
+ switch (trackingStatus) {
+ case ClientData.ALLOCATION_TRACKING_UNKNOWN:
+ mEnableButton.setText("?");
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ break;
+ case ClientData.ALLOCATION_TRACKING_OFF:
+ mEnableButton.setText("Start Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(false);
+ break;
+ case ClientData.ALLOCATION_TRACKING_ON:
+ mEnableButton.setText("Stop Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(true);
+ break;
+ }
+ } else {
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ mEnableButton.setText("Start Tracking");
+ }
+ }
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java
new file mode 100644
index 0000000..0ed4c95
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java
new file mode 100644
index 0000000..3e66ea5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java
new file mode 100644
index 0000000..a711933
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java
new file mode 100644
index 0000000..f832a4e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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;
+ }
+
+ static String getSymbolDirectory() {
+ return sSymbolLocation;
+ }
+
+ public static void setSymbolsLocation(String location) {
+ sSymbolLocation = location;
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java
new file mode 100644
index 0000000..81b757e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java
@@ -0,0 +1,744 @@
+/*
+ * 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.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Device.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;
+
+/**
+ * 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$
+
+ private Device 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<Device> mDevicesToExpand = new ArrayList<Device>();
+
+ private IImageLoader mLoader;
+
+ private boolean mAdvancedPortSupport;
+
+ /**
+ * A Content provider for the {@link TreeViewer}.
+ * <p/>
+ * The input is a {@link AndroidDebugBridge}. First level elements are {@link Device} objects,
+ * and second level elements are {@link Client} object.
+ */
+ private class ContentProvider implements ITreeContentProvider {
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof Device) {
+ return ((Device)parentElement).getClients();
+ }
+ return new Object[0];
+ }
+
+ public Object getParent(Object element) {
+ if (element instanceof Client) {
+ return ((Client)element).getDevice();
+ }
+ return null;
+ }
+
+ public boolean hasChildren(Object element) {
+ if (element instanceof Device) {
+ return ((Device)element).hasClients();
+ }
+
+ // Clients never have children.
+ return false;
+ }
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof AndroidDebugBridge) {
+ return ((AndroidDebugBridge)inputElement).getDevices();
+ }
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ 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 Device} and {@link Client} objects.
+ */
+ private class LabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == DEVICE_COL_SERIAL && element instanceof Device) {
+ Device device = (Device)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 ClientData.DEBUGGER_DEFAULT:
+ return null;
+ case ClientData.DEBUGGER_WAITING:
+ return mWaitingImage;
+ case ClientData.DEBUGGER_ATTACHED:
+ return mDebuggerImage;
+ case ClientData.DEBUGGER_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;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof Device) {
+ Device device = (Device)element;
+ switch (columnIndex) {
+ case DEVICE_COL_SERIAL:
+ return device.getSerialNumber();
+ case DEVICE_COL_STATE:
+ return getStateString(device);
+ case DEVICE_COL_BUILD: {
+ String version = device.getProperty(Device.PROP_BUILD_VERSION);
+ if (version != null) {
+ String debuggable = device.getProperty(Device.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) {
+ 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;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deals
+ * with {@link Device} and {@link Client} selection changes coming from the ui.
+ */
+ public interface IUiSelectionListener {
+ /**
+ * Sent when a new {@link Device} 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(Device 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(IImageLoader loader, boolean advancedPortSupport) {
+ mLoader = loader;
+ 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(), mLoader);
+
+ 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), Device 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 Device}. If a {@link Client} is selected, it returns the
+ * Device object containing the client.
+ */
+ public Device 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 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)
+ */
+ public void bridgeChanged(final AndroidDebugBridge bridge) {
+ if (mTree.isDisposed() == false) {
+ exec(new Runnable() {
+ 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(Device)
+ */
+ public void deviceConnected(Device device) {
+ exec(new Runnable() {
+ 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(Device)
+ */
+ public void deviceDisconnected(Device 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(Device)
+ */
+ public void deviceChanged(final Device 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() {
+ 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.
+
+ Device 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_INTEREST}, {@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)
+ */
+ public void clientChanged(final Client client, final int changeMask) {
+ exec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // refresh the client
+ mTreeViewer.refresh(client);
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) ==
+ Client.CHANGE_DEBUGGER_INTEREST &&
+ client.getClientData().getDebuggerConnectionStatus() ==
+ ClientData.DEBUGGER_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.
+ Device 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, IImageLoader loader) {
+ if (mDeviceImage == null) {
+ mDeviceImage = ImageHelper.loadImage(loader, display, "device.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mEmulatorImage == null) {
+ mEmulatorImage = ImageHelper.loadImage(loader, display,
+ "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mThreadImage == null) {
+ mThreadImage = ImageHelper.loadImage(loader, display, ICON_THREAD,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_YELLOW));
+ }
+ if (mHeapImage == null) {
+ mHeapImage = ImageHelper.loadImage(loader, display, ICON_HEAP,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mWaitingImage == null) {
+ mWaitingImage = ImageHelper.loadImage(loader, display,
+ "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mDebuggerImage == null) {
+ mDebuggerImage = ImageHelper.loadImage(loader, display,
+ "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_GREEN));
+ }
+ if (mDebugErrorImage == null) {
+ mDebugErrorImage = ImageHelper.loadImage(loader, 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(Device 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;
+ Device 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 Device) {
+ device = (Device)object;
+ }
+ }
+
+ notifyListeners(device, client);
+ }
+
+ private void notifyListeners(Device 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/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java
new file mode 100644
index 0000000..5583760
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java
@@ -0,0 +1,1454 @@
+/*
+ * 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.Device;
+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.ddmuilib.location.CoordinateControls;
+import com.android.ddmuilib.location.GpxParser;
+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 com.android.ddmuilib.location.GpxParser.Track;
+
+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 IImageLoader mImageLoader;
+
+ 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(IImageLoader imageLoader) {
+ mImageLoader = imageLoader;
+ }
+
+ /**
+ * 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() {
+ 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-1$
+
+ // replace remaining stand alone \n
+ message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$
+
+ // replace remaining stand alone \r
+ message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$
+
+ 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);
+
+ mPlayImage = mImageLoader.loadImage("play.png", mParent.getDisplay()); // $NON-NLS-1$
+ mPauseImage = mImageLoader.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() {
+ 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() {
+ 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;
+
+ mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mGpxBackwardButton.setSelection(false);
+ mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxForwardButton.setImage(mImageLoader.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() {
+ 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;
+
+ mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mKmlBackwardButton.setSelection(false);
+ mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlForwardButton.setImage(mImageLoader.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(Device 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java
new file mode 100644
index 0000000..977203b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java
@@ -0,0 +1,1294 @@
+/*
+ * 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.ClientData;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+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_INTEREST}, {@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)
+ */
+ 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() {
+ 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(ImageHelper.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(ImageHelper.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");
+
+ 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() {
+ 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();
+
+ if (cd != null) {
+ synchronized (cd) {
+ Iterator<Integer> iter = cd.getVmHeapIds();
+
+ while (iter.hasNext()) {
+ 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));
+ }
+ }
+ }
+
+ 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);
+ }
+
+ 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((double)((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/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java
new file mode 100644
index 0000000..bcbf612
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java
@@ -0,0 +1,45 @@
+/*
+ * 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.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Interface defining an image loader. jar app/lib and plugin have different packaging method
+ * so each implementation will be different.
+ * The implementation should implement at least one of the methods, and preferably both if possible.
+ *
+ */
+public interface IImageLoader {
+
+ /**
+ * Load an image from the resource from a filename
+ * @param filename
+ * @param display
+ */
+ public Image loadImage(String filename, Display display);
+
+ /**
+ * Load an ImageDescriptor from the resource from a filename
+ * @param filename
+ * @param display
+ */
+ public ImageDescriptor loadDescriptor(String filename, Display display);
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java
new file mode 100644
index 0000000..bf425d9
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java
new file mode 100644
index 0000000..d65978b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.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.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+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;
+
+public class ImageHelper {
+
+ /**
+ * 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 loader the image loader used.
+ * @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 static Image loadImage(IImageLoader loader, Display display,
+ String fileName, int width, int height, Color phColor) {
+
+ Image img = null;
+ if (loader != null) {
+ img = loader.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/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java
new file mode 100644
index 0000000..76f2285
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java
@@ -0,0 +1,62 @@
+/*
+ * 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.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.InputStream;
+
+/**
+ * Image loader for an normal standalone app.
+ */
+public class ImageLoader implements IImageLoader {
+
+ /** class used as reference to get the reources */
+ private Class<?> mClass;
+
+ /**
+ * Creates a loader for a specific class. The class allows java to figure
+ * out which .jar file to search for the image.
+ *
+ * @param theClass
+ */
+ public ImageLoader(Class<?> theClass) {
+ mClass = theClass;
+ }
+
+ public ImageDescriptor loadDescriptor(String filename, Display display) {
+ // we don't support ImageDescriptor
+ return null;
+ }
+
+ public Image loadImage(String filename, Display display) {
+
+ String tmp = "/images/" + filename;
+ InputStream imageStream = mClass.getResourceAsStream(tmp);
+
+ if (imageStream != null) {
+ Image img = new Image(display, imageStream);
+ if (img == null)
+ throw new NullPointerException("couldn't load " + tmp);
+ return img;
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java
new file mode 100644
index 0000000..72cbb4a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java
@@ -0,0 +1,175 @@
+/*
+ * 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.ClientData;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+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:",
+ };
+ 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;
+
+ /**
+ * 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_INTEREST}, {@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)
+ */
+ 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() {
+ 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);
+ }
+
+ mCol2.pack();
+
+ //Log.i("ddms", "InfoPanel: changed " + client);
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mTable);
+ }
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java
new file mode 100644
index 0000000..46461bf
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java
@@ -0,0 +1,1633 @@
+/*
+ * 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.ClientData;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+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;
+
+/**
+ * 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 Long[] list = info.getStackCallAddresses();
+ final int size = list.length;
+
+ ArrayList<NativeStackCallInfo> resolvedStackCall =
+ new ArrayList<NativeStackCallInfo>();
+
+ for (int i = 0; i < size; i++) {
+ long addr = list[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.getLibraryName());
+ if (process != null) {
+ // remove the base of the library address
+ long value = addr - library.getStartAddress();
+ NativeStackCallInfo info = process.getAddress(value);
+ if (info != null) {
+ return info;
+ }
+ }
+ }
+
+ return new NativeStackCallInfo(library != null ? library.getLibraryName() : null,
+ Long.toHexString(addr), "");
+ }
+
+ private NativeLibraryMapInfo getLibraryFor(long addr) {
+ Iterator<NativeLibraryMapInfo> it = mClientData.getNativeLibraryMapInfo();
+
+ while (it.hasNext()) {
+ NativeLibraryMapInfo info = it.next();
+
+ 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() {
+ 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() {
+ 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>() {
+ 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_INTEREST}, {@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)
+ */
+ 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() {
+ 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() {
+ 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
+ Long[] addresses = mi.getStackCallAddresses();
+ NativeStackCallInfo[] resolvedStackCall = mi.getResolvedStackCall();
+
+ if (resolvedStackCall == null) {
+ return;
+ }
+
+ for (int i = 0 ; i < resolvedStackCall.length ; i++) {
+ if (addresses[i] == null || addresses[i].longValue() == 0) {
+ continue;
+ }
+
+ long addr = addresses[i].longValue();
+ NativeStackCallInfo source = resolvedStackCall[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() {
+ 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();
+ 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
+ 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() {
+ 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>() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/Panel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java
new file mode 100644
index 0000000..d910cc7
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java
new file mode 100644
index 0000000..533372e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java
new file mode 100644
index 0000000..dad54dc
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java
@@ -0,0 +1,256 @@
+/*
+ * 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.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.RawImage;
+
+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.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+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.IOException;
+
+
+/**
+ * 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 Device mDevice;
+
+
+ /**
+ * Create with default style.
+ */
+ public ScreenShotDialog(Shell parent) {
+ this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Create with app-defined style.
+ */
+ public ScreenShotDialog(Shell parent, int style) {
+ super(parent, style);
+ }
+
+ /**
+ * Prepare and display the dialog.
+ * @param device The {@link Device} from which to get the screenshot.
+ */
+ public void open(Device 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;
+
+ shell.setLayout(new GridLayout(3, true));
+
+ // title/"capturing" label
+ mBusyLabel = new Label(shell, SWT.NONE);
+ mBusyLabel.setText("Preparing...");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+ data.horizontalSpan = 3;
+ mBusyLabel.setLayoutData(data);
+
+ // space for the image
+ mImageLabel = new Label(shell, SWT.BORDER);
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.horizontalSpan = 3;
+ mImageLabel.setLayoutData(data);
+ Display display = shell.getDisplay();
+ mImageLabel.setImage(ImageHelper.createPlaceHolderArt(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)));
+
+ // "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);
+ }
+ });
+
+ // "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);
+ }
+ });
+
+ // "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();
+ }
+ });
+
+ shell.setDefaultButton(done);
+ }
+
+ /*
+ * Capture a new image from the device.
+ */
+ private void updateDeviceImage(Shell shell) {
+ mBusyLabel.setText("Capturing..."); // no effect
+
+ shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
+
+ Image image = getDeviceImage();
+ if (image == null) {
+ Display display = shell.getDisplay();
+ image = ImageHelper.createPlaceHolderArt(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE));
+ mSave.setEnabled(false);
+ mBusyLabel.setText("Screen not available");
+ } else {
+ 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));
+ }
+
+ /*
+ * Grab an image from an ADB-connected device.
+ */
+ private Image getDeviceImage() {
+ RawImage rawImage;
+
+ try {
+ rawImage = mDevice.getScreenshot();
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage());
+ return null;
+ }
+
+ // device/adb not available?
+ if (rawImage == null)
+ return null;
+
+ // convert raw data to an Image
+ assert rawImage.bpp == 16;
+ PaletteData palette = new PaletteData(0xf800, 0x07e0, 0x001f);
+ ImageData imageData = new ImageData(rawImage.width, rawImage.height,
+ rawImage.bpp, palette, 1, rawImage.data);
+
+ return new Image(getParent().getDisplay(), imageData);
+ }
+
+ /*
+ * Prompt the user to save the image to disk.
+ */
+ private void saveImage(Shell shell) {
+ FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+ String fileName;
+
+ dlg.setText("Save image...");
+ dlg.setFileName("device.png");
+ dlg.setFilterPath(DdmUiPreferences.getStore().getString("lastImageSaveDir"));
+ dlg.setFilterNames(new String[] {
+ "PNG Files (*.png)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.png" //$NON-NLS-1$
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ DdmUiPreferences.getStore().setValue("lastImageSaveDir", dlg.getFilterPath());
+
+ Log.i("ddms", "Saving image to " + fileName);
+ ImageData imageData = mImageLabel.getImage().getImageData();
+
+ try {
+ WritePng.savePng(fileName, imageData);
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to save " + fileName + ": " + ioe);
+ }
+
+ if (false) {
+ ImageLoader loader = new ImageLoader();
+ loader.data = new ImageData[] { imageData };
+ // PNG writing not available until 3.3? See bug at:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=24697
+ // GIF writing only works for 8 bits
+ // JPEG uses lossy compression
+ // BMP has screwed-up colors
+ loader.save(fileName, SWT.IMAGE_JPEG);
+ }
+ }
+ }
+
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java
new file mode 100644
index 0000000..4b5fe56
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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.Device;
+
+/**
+ * A Panel that requires {@link Device}/{@link Client} selection notifications.
+ */
+public abstract class SelectionDependentPanel extends Panel {
+ private Device mCurrentDevice = null;
+ private Client mCurrentClient = null;
+
+ /**
+ * Returns the current {@link Device}.
+ * @return the current device or null if none are selected.
+ */
+ protected final Device 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(Device 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/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java
new file mode 100644
index 0000000..3358962
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java
@@ -0,0 +1,258 @@
+/*
+ * 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, String, String, String, 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 {
+ 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];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ 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 {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof StackTraceElement) {
+ StackTraceElement traceElement = (StackTraceElement)element;
+ switch (columnIndex) {
+ case 0:
+ return traceElement.getClassName();
+ case 1:
+ return traceElement.getMethodName();
+ case 2:
+ return traceElement.getFileName();
+ case 3:
+ return Integer.toString(traceElement.getLineNumber());
+ case 4:
+ return Boolean.toString(traceElement.isNativeMethod());
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ 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_col_class
+ * @param prefs_stack_col_method
+ * @param prefs_stack_col_file
+ * @param prefs_stack_col_line
+ * @param prefs_stack_col_native
+ * @param store
+ */
+ public Table createPanel(Composite parent, String prefs_stack_col_class,
+ String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line,
+ String prefs_stack_col_native, IPreferenceStore store) {
+
+ mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mStackTraceTable.setHeaderVisible(true);
+ mStackTraceTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Class",
+ SWT.LEFT,
+ "SomeLongClassName", //$NON-NLS-1$
+ prefs_stack_col_class, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Method",
+ SWT.LEFT,
+ "someLongMethod", //$NON-NLS-1$
+ prefs_stack_col_method, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "File",
+ SWT.LEFT,
+ "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$
+ prefs_stack_col_file, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Line",
+ SWT.RIGHT,
+ "99999", //$NON-NLS-1$
+ prefs_stack_col_line, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Native",
+ SWT.LEFT,
+ "Native", //$NON-NLS-1$
+ prefs_stack_col_native, store);
+
+ mStackTraceViewer = new TableViewer(mStackTraceTable);
+ mStackTraceViewer.setContentProvider(new StackTraceContentProvider());
+ mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider());
+
+ mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java
new file mode 100644
index 0000000..8ef237c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java
@@ -0,0 +1,582 @@
+/*
+ * 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.IShellOutputReceiver;
+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.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.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+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.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;
+
+ // 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 static final int MODE_CPU = 0;
+ private static final int MODE_ALARM = 1;
+ private static final int MODE_WAKELOCK = 2;
+ private static final int MODE_MEMINFO = 3;
+ private static final int MODE_SYNC = 4;
+
+ // argument to dumpsys; section in the bugreport holding the data
+ private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm",
+ "batteryinfo", "MEMORY INFO", "content"};
+
+ private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
+ "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
+ "dumpsys content"};
+
+ private static final String CAPTIONS[] = {"CPU load", "Alarms",
+ "Wakelocks", "Memory usage", "Sync"};
+
+ /**
+ * Generates the dataset to display.
+ *
+ * @param file The bugreport file to process.
+ */
+ public void generateDataset(File file) {
+ mDataset.clear();
+ mLabel.setText("");
+ if (file == null) {
+ return;
+ }
+ try {
+ BufferedReader br = getBugreportReader(file);
+ if (mMode == MODE_CPU) {
+ readCpuDataset(br);
+ } else if (mMode == MODE_ALARM) {
+ readAlarmDataset(br);
+ } else if (mMode == MODE_WAKELOCK) {
+ readWakelockDataset(br);
+ } else if (mMode == MODE_MEMINFO) {
+ readMeminfoDataset(br);
+ } else if (mMode == MODE_SYNC) {
+ readSyncDataset(br);
+ }
+ } 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() {
+ try {
+ initShellOutputBuffer();
+ if (mMode == MODE_MEMINFO) {
+ // Hack to add bugreport-style section header for meminfo
+ mTempStream.write("------ MEMORY INFO ------\n".getBytes());
+ }
+ getCurrentDevice().executeShellCommand(
+ DUMP_COMMAND[mMode], this);
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * 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().
+ */
+ 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.
+ */
+ 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
+ */
+ 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();
+ }
+ }
+ });
+
+ final Button loadButton = new Button(buttons, SWT.PUSH);
+ loadButton.setText("Load from File");
+ loadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(loadButton.getShell(),
+ SWT.OPEN);
+ fileDialog.setText("Load bugreport");
+ String filename = fileDialog.open();
+ if (filename != null) {
+ mDataFile = new File(filename);
+ generateDataset(mDataFile);
+ }
+ }
+ });
+
+ 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));
+
+ mDataset = new DefaultPieDataset();
+ JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
+ /* legend */, true/* tooltips */, false /* urls */);
+
+ ChartComposite chartComposite = new ChartComposite(top,
+ 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);
+ chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return top;
+ }
+
+ 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 {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ // Skip over the unwanted bugreport sections
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ Log.d("DDMS", "Service not found " + line);
+ break;
+ }
+ if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
+ line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
+ break;
+ }
+ }
+ return br;
+ }
+
+ /**
+ * 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;
+ }
+ /**
+ * Processes wakelock information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readWakelockDataset(BufferedReader br) throws IOException {
+ 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.;
+ mDataset.setValue(m.group(1), value);
+ total -= value;
+ } else {
+ m = totalPattern.matcher(line);
+ if (m.find()) {
+ total += parseTimeMs(m.group(1)) / 1000.;
+ }
+ }
+ }
+ }
+ if (total > 0) {
+ mDataset.setValue("Unlocked", total);
+ }
+ }
+
+ /**
+ * Processes alarm information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readAlarmDataset(BufferedReader br) throws IOException {
+ 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);
+ mDataset.setValue(name, count);
+ }
+ }
+ }
+
+ /**
+ * Processes cpu load information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readCpuDataset(BufferedReader br) throws IOException {
+ Pattern pattern = Pattern
+ .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
+
+ 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("Load:")) {
+ mLabel.setText(line);
+ continue;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ String name = m.group(1);
+ long both = Long.parseLong(m.group(2));
+ long user = Long.parseLong(m.group(3));
+ long kernel = Long.parseLong(m.group(4));
+ if ("TOTAL".equals(name)) {
+ if (both < 100) {
+ mDataset.setValue("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) {
+ mDataset.setValue(name + " (user)", user);
+ }
+ if (kernel > 0) {
+ mDataset.setValue(name + " (kernel)" , both - user);
+ }
+ if (user == 0 && kernel == 0 && both > 0) {
+ mDataset.setValue(name, both);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes meminfo information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readMeminfoDataset(BufferedReader br) throws IOException {
+ Pattern valuePattern = Pattern.compile("(\\d+) kB");
+ long total = 0;
+ long other = 0;
+ mLabel.setText("PSS in kB");
+
+ // Scan meminfo
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ // End of file
+ break;
+ }
+ 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")) {
+ mDataset.setValue("Free", kb);
+ total -= kb;
+ } else if (line.startsWith("Slab")) {
+ mDataset.setValue("Slab", kb);
+ total -= kb;
+ } else if (line.startsWith("PageTables")) {
+ mDataset.setValue("PageTables", kb);
+ total -= kb;
+ } else if (line.startsWith("Buffers") && kb > 0) {
+ mDataset.setValue("Buffers", kb);
+ total -= kb;
+ } else if (line.startsWith("Inactive")) {
+ mDataset.setValue("Inactive", kb);
+ total -= kb;
+ } else if (line.startsWith("MemFree")) {
+ mDataset.setValue("Free", kb);
+ total -= kb;
+ }
+ } else {
+ break;
+ }
+ }
+ // Scan procrank
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ break;
+ }
+ if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
+ // procrank header
+ continue;
+ }
+ if (line.indexOf("----") >= 0) {
+ //end of procrank section
+ break;
+ }
+ // Extract pss field from procrank output
+ long pss = Long.parseLong(line.substring(23, 31).trim());
+ String cmdline = line.substring(43).trim().replace("/system/bin/", "");
+ // Arbitrary minimum size to display
+ if (pss > 2000) {
+ mDataset.setValue(cmdline, pss);
+ } else {
+ other += pss;
+ }
+ total -= pss;
+ }
+ mDataset.setValue("Other", other);
+ mDataset.setValue("Unknown", total);
+ }
+
+ /**
+ * Processes sync information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readSyncDataset(BufferedReader br) throws IOException {
+ 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]);
+ mDataset.setValue(authority, dur);
+ } else if (duration.length() == 3) {
+ long dur = Long.parseLong(durParts[0]) * 3600
+ + Long.parseLong(durParts[1]) * 60 + Long
+ .parseLong(durParts[2]);
+ mDataset.setValue(authority, dur);
+ }
+ }
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java
new file mode 100644
index 0000000..f8d457e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java
@@ -0,0 +1,149 @@
+/*
+ * 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() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ 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() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java
new file mode 100644
index 0000000..b037193
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java
@@ -0,0 +1,128 @@
+/*
+ * 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 final 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() {
+ 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() });
+ }
+ }
+
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java
new file mode 100644
index 0000000..a034063
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java
@@ -0,0 +1,572 @@
+/*
+ * 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.ThreadInfo;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+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_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$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", "running", "timed-wait", "monitor",
+ "wait", "init", "start", "native", "vmwait"
+ };
+
+ /**
+ * Content Provider to display the threads of a client.
+ * Expected input is a {@link Client} object.
+ */
+ private static class ThreadContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ return ((Client)inputElement).getClientData().getThreads();
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ 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 {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ 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;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ 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() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ updateThreadStackTrace(selectedThread);
+ }
+ });
+ mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ if (selectedThread != null) {
+ Client client = (Client)mThreadViewer.getInput();
+
+ if (client != null) {
+ client.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ // 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) {
+ ThreadInfo selectedThread = getThreadSelection(null);
+ if (selectedThread != null) {
+ Client currentClient = getCurrentClient();
+ if (currentClient != null) {
+ currentClient.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
+ mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase,
+ PREFS_STACK_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ 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() {
+ 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_INTEREST}, {@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)
+ */
+ 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() {
+ 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() {
+ 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();
+ }
+
+ /**
+ * 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java
new file mode 100644
index 0000000..f65dafe
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java
@@ -0,0 +1,258 @@
+/*
+ * 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.swt.graphics.ImageData;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+/**
+ * Compensate for SWT issues by writing our own PNGs from ImageData.
+ */
+public class WritePng {
+ private WritePng() {}
+
+ private static final byte[] PNG_MAGIC =
+ new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 };
+
+ public static void savePng(String fileName, ImageData imageData)
+ throws IOException {
+
+ try {
+ FileOutputStream out = new FileOutputStream(fileName);
+
+ Log.d("ddms", "Saving to PNG, width=" + imageData.width
+ + ", height=" + imageData.height
+ + ", depth=" + imageData.depth
+ + ", bpl=" + imageData.bytesPerLine);
+
+ savePng(out, imageData);
+
+ // need to do that on, or the file is empty on windows!
+ out.flush();
+ out.close();
+ } catch (Exception e) {
+ Log.e("writepng", e);
+ }
+ }
+
+ /*
+ * Supply functionality missing from our version of SWT.
+ */
+ private static void savePng(OutputStream out, ImageData imageData)
+ throws IOException {
+
+ int width = imageData.width;
+ int height = imageData.height;
+ byte[] out24;
+
+ Log.i("ddms-png", "Convert to 24bit from " + imageData.depth);
+
+ if (imageData.depth == 24 || imageData.depth == 32) {
+ out24 = convertTo24ForPng(imageData.data, width, height,
+ imageData.depth, imageData.bytesPerLine);
+ } else if (imageData.depth == 16) {
+ out24 = convert16to24(imageData);
+ } else {
+ return;
+ }
+
+ // Create the compressed form. I'm taking the low road here and
+ // just creating a large buffer, which should always be enough to
+ // hold the compressed output.
+ byte[] compPixels = new byte[out24.length + 16384];
+ Deflater compressor = new Deflater();
+ compressor.setLevel(Deflater.BEST_COMPRESSION);
+ compressor.setInput(out24);
+ compressor.finish();
+ int compLen;
+ do { // must do this in a loop to satisfy java.util.Zip
+ compLen = compressor.deflate(compPixels);
+ assert compLen != 0 || !compressor.needsInput();
+ } while (compLen == 0);
+ Log.d("ddms", "Compressed image data from " + out24.length
+ + " to " + compLen);
+
+ // Write the PNG magic
+ out.write(PNG_MAGIC);
+
+ ByteBuffer buf;
+ CRC32 crc;
+
+ // Write the IHDR chunk (13 bytes)
+ byte[] header = new byte[8 + 13 + 4];
+ buf = ByteBuffer.wrap(header);
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ putChunkHeader(buf, 13, "IHDR");
+ buf.putInt(width);
+ buf.putInt(height);
+ buf.put((byte) 8); // 8pp
+ buf.put((byte) 2); // direct color used
+ buf.put((byte) 0); // compression method == deflate
+ buf.put((byte) 0); // filter method (none)
+ buf.put((byte) 0); // interlace method (none)
+
+ crc = new CRC32();
+ crc.update(header, 4, 4+13);
+ buf.putInt((int)crc.getValue());
+
+ out.write(header);
+
+ // Write the IDAT chunk
+ byte[] datHdr = new byte[8 + 0 + 4];
+ buf = ByteBuffer.wrap(datHdr);
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ putChunkHeader(buf, compLen, "IDAT");
+ crc = new CRC32();
+ crc.update(datHdr, 4, 4+0);
+ crc.update(compPixels, 0, compLen);
+ buf.putInt((int) crc.getValue());
+
+ out.write(datHdr, 0, 8);
+ out.write(compPixels, 0, compLen);
+ out.write(datHdr, 8, 4);
+
+ // Write the IEND chunk (0 bytes)
+ byte[] trailer = new byte[8 + 0 + 4];
+
+ buf = ByteBuffer.wrap(trailer);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ putChunkHeader(buf, 0, "IEND");
+
+ crc = new CRC32();
+ crc.update(trailer, 4, 4+0);
+ buf.putInt((int)crc.getValue());
+
+ out.write(trailer);
+ }
+
+ /*
+ * Output a chunk header.
+ */
+ private static void putChunkHeader(ByteBuffer buf, int length,
+ String typeStr) {
+
+ int type = 0;
+
+ if (typeStr.length() != 4)
+ throw new RuntimeException();
+
+ for (int i = 0; i < 4; i++) {
+ type <<= 8;
+ type |= (byte) typeStr.charAt(i);
+ }
+
+ buf.putInt(length);
+ buf.putInt(type);
+ }
+
+ /*
+ * Convert raw pixels to 24-bit RGB with a "filter" byte at the start
+ * of each row.
+ */
+ private static byte[] convertTo24ForPng(byte[] in, int width, int height,
+ int depth, int stride) {
+
+ assert depth == 24 || depth == 32;
+ assert stride == width * (depth/8);
+
+ // 24 bit pixels plus one byte per line for "filter"
+ byte[] out24 = new byte[width * height * 3 + height];
+ int y;
+
+ int inOff = 0;
+ int outOff = 0;
+ for (y = 0; y < height; y++) {
+ out24[outOff++] = 0; // filter flag
+
+ if (depth == 24) {
+ System.arraycopy(in, inOff, out24, outOff, width * 3);
+ outOff += width * 3;
+ } else if (depth == 32) {
+ int tmpOff = inOff;
+ for (int x = 0; x < width; x++) {
+ tmpOff++; // ignore alpha
+ out24[outOff++] = in[tmpOff++];
+ out24[outOff++] = in[tmpOff++];
+ out24[outOff++] = in[tmpOff++];
+ }
+ }
+
+ inOff += stride;
+ }
+
+ assert outOff == out24.length;
+
+ return out24;
+ }
+
+ private static byte[] convert16to24(ImageData imageData) {
+ int width = imageData.width;
+ int height = imageData.height;
+
+ int redShift = imageData.palette.redShift;
+ int greenShift = imageData.palette.greenShift;
+ int blueShift = imageData.palette.blueShift;
+
+ int redMask = imageData.palette.redMask;
+ int greenMask = imageData.palette.greenMask;
+ int blueMask = imageData.palette.blueMask;
+
+ // 24 bit pixels plus one byte per line for "filter"
+ byte[] out24 = new byte[width * height * 3 + height];
+ int outOff = 0;
+
+
+ int[] line = new int[width];
+ for (int y = 0; y < height; y++) {
+ imageData.getPixels(0, y, width, line, 0);
+
+ out24[outOff++] = 0; // filter flag
+ for (int x = 0; x < width; x++) {
+ int pixelValue = line[x];
+ out24[outOff++] = byteChannelValue(pixelValue, redMask, redShift);
+ out24[outOff++] = byteChannelValue(pixelValue, greenMask, greenShift);
+ out24[outOff++] = byteChannelValue(pixelValue, blueMask, blueShift);
+ }
+ }
+
+ return out24;
+ }
+
+ private static byte byteChannelValue(int value, int mask, int shift) {
+ int bValue = value & mask;
+ if (shift < 0) {
+ bValue = bValue >>> -shift;
+ } else {
+ bValue = bValue << shift;
+ }
+
+ return (byte)bValue;
+
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java
new file mode 100644
index 0000000..856b874
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java
new file mode 100644
index 0000000..bc1598f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java
@@ -0,0 +1,68 @@
+/*
+ * 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)
+ */
+ 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)
+ */
+ 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)
+ */
+ public void setRunnable(final Runnable runnable) {
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ runnable.run();
+ }
+ });
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java
new file mode 100644
index 0000000..8e9e11b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface UiThread {
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java
new file mode 100644
index 0000000..e767eda
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface WorkerThread {
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java
new file mode 100644
index 0000000..4df4376
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java
new file mode 100644
index 0000000..3679d41
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java
new file mode 100644
index 0000000..75c19fe
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java
@@ -0,0 +1,167 @@
+/*
+ * 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() {
+ 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() {
+ 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);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ 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() {
+ 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)
+ */
+ 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)
+ */
+ 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)
+ */
+ 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)
+ */
+ 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()
+ */
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
+ */
+ 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/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java
new file mode 100644
index 0000000..ba0f555
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java
@@ -0,0 +1,833 @@
+/*
+ * 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.Device;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.SyncService.SyncResult;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.Panel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+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.lang.reflect.InvocationTargetException;
+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 Image mFileImage;
+ private Image mFolderImage;
+ private Image mPackageImage;
+ private Image mOtherImage;
+
+ private Device mCurrentDevice;
+
+ private String mDefaultSave;
+
+ /**
+ * Implementation of the SyncService.ISyncProgressMonitor. It wraps a jFace IProgressMonitor
+ * and just forward the calls to the jFace object.
+ */
+ private static class SyncProgressMonitor implements ISyncProgressMonitor {
+
+ private IProgressMonitor mMonitor;
+ private String mName;
+
+ SyncProgressMonitor(IProgressMonitor monitor, String name) {
+ mMonitor = monitor;
+ mName = name;
+ }
+
+ public void start(int totalWork) {
+ mMonitor.beginTask(mName, totalWork);
+ }
+
+ public void stop() {
+ mMonitor.done();
+ }
+
+ public void advance(int work) {
+ mMonitor.worked(work);
+ }
+
+ public boolean isCanceled() {
+ return mMonitor.isCanceled();
+ }
+
+ public void startSubTask(String name) {
+ mMonitor.subTask(name);
+ }
+ }
+
+ public DeviceExplorer() {
+
+ }
+
+ /**
+ * Sets the images for the listview
+ * @param fileImage
+ * @param folderImage
+ * @param otherImage
+ */
+ public void setImages(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
+ */
+ public void setActions(ICommonAction pushAction, ICommonAction pullAction,
+ ICommonAction deleteAction) {
+ mPushAction = pushAction;
+ mPullAction = pullAction;
+ mDeleteAction = deleteAction;
+ }
+
+ /**
+ * 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());
+
+ 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() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection sel = event.getSelection();
+ if (sel.isEmpty()) {
+ mPullAction.setEnabled(false);
+ mPushAction.setEnabled(false);
+ mDeleteAction.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) {
+ setDeleteEnabledState((FileEntry)element);
+ } else {
+ mDeleteAction.setEnabled(false);
+ }
+ }
+ }
+ }
+ });
+
+ // add support for double click
+ mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ 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() {
+ 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, ".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
+ SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
+ SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage()));
+ return;
+ }
+
+ result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage()));
+ return;
+ }
+
+ // 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) {
+ }
+ }
+ }
+
+ /**
+ * 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() {
+ public void addOutput(byte[] data, int offset, int length) {
+ // pass
+ // TODO get output to display errors if any.
+ }
+
+ public void flush() {
+ mTreeViewer.refresh(parentEntry);
+ }
+
+ 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.
+ }
+
+ }
+
+ /**
+ * Force a full refresh of the explorer.
+ */
+ public void refresh() {
+ mTreeViewer.refresh(true);
+ }
+
+ /**
+ * Sets the new device to explorer
+ */
+ public void switchDevice(final Device 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() {
+ 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() {
+ 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) {
+ 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()]);
+
+ // get a progressdialog
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ // create a monitor wrapper around the jface monitor
+ SyncResult result = sync.pull(entryArray, localDirectory,
+ new SyncProgressMonitor(monitor,
+ "Pulling file(s) from the device"));
+
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull selection: %1$s", result.getMessage()));
+ }
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException 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) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor(
+ monitor, String.format("Pulling %1$s from the device",
+ remote.getName())));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", remote, result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException 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) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ SyncResult result = sync.push(localFiles, remoteDirectory,
+ new SyncProgressMonitor(monitor,
+ "Pushing file(s) to the device"));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push the items: %1$s", result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole("Failed to push the items");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to push the items");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ return;
+ }
+ }
+
+ /**
+ * 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) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ // get the file name
+ String[] segs = local.split(Pattern.quote(File.separator));
+ String name = segs[segs.length-1];
+ String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
+ + name;
+
+ SyncResult result = sync.pushFile(local, remoteFile,
+ new SyncProgressMonitor(monitor,
+ String.format("Pushing %1$s to the device.", name)));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push %1$s on %2$s: %3$s",
+ name, mCurrentDevice.getSerialNumber(), result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole("Failed to push the item(s).");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to push the item(s).");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ return;
+ }
+ }
+
+ /**
+ * 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/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java
new file mode 100644
index 0000000..1dca962
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java
@@ -0,0 +1,152 @@
+/*
+ * 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)
+ */
+ public Image getImage(Object element) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
+ */
+ public String getText(Object element) {
+ return null;
+ }
+
+ 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;
+ }
+
+ 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)
+ */
+ public void addListener(ILabelProviderListener listener) {
+ // we don't need listeners.
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
+ */
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
+ */
+ public boolean isLabelProperty(Object element, String property) {
+ return false;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
+ */
+ public void removeListener(ILabelProviderListener listener) {
+ // we don't need listeners
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java
new file mode 100644
index 0000000..578a7ac
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java
@@ -0,0 +1,243 @@
+/*
+ * 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;
+
+/**
+ * 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;
+
+ /** 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() {
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = getValueFromSexagesimalControls();
+ setValueIntoDecimalControl(mValue);
+ mValueValidity = true;
+ } catch (NumberFormatException 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() {
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = Double.parseDouble(mDecimalText.getText());
+ setValueIntoSexagesimalControl(mValue);
+ mValueValidity = true;
+ } catch (NumberFormatException 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 NumberFormatException {
+ double degrees = Double.parseDouble(mSexagesimalDegreeText.getText());
+ double minutes = Double.parseDouble(mSexagesimalMinuteText.getText());
+ double seconds = Double.parseDouble(mSexagesimalSecondText.getText());
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java
new file mode 100644
index 0000000..a30337a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java
new file mode 100644
index 0000000..af485ac
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java
new file mode 100644
index 0000000..dbb8f41
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java
new file mode 100644
index 0000000..7fb37ce
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Track[]) {
+ return (Track[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java
new file mode 100644
index 0000000..81d1f7d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java
@@ -0,0 +1,81 @@
+/*
+ * 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 {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ 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;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java
new file mode 100644
index 0000000..527f4bf
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java
new file mode 100644
index 0000000..32880bd
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java
new file mode 100644
index 0000000..fced777
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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 {
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof WayPoint[]) {
+ return (WayPoint[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java
new file mode 100644
index 0000000..f5e6f1b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java
@@ -0,0 +1,73 @@
+/*
+ * 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 {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ 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;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java
new file mode 100644
index 0000000..9de1ac7
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java
@@ -0,0 +1,89 @@
+/*
+ * 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) {
+ }
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java
new file mode 100644
index 0000000..473387a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java
new file mode 100644
index 0000000..0cffd7e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java
new file mode 100644
index 0000000..26296f3
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java
@@ -0,0 +1,379 @@
+/*
+ * 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 = (long) 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 = (long) 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java
new file mode 100644
index 0000000..82cc7a4
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java
@@ -0,0 +1,293 @@
+/*
+ * 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];
+ mTooltipsSync = new List[NUM_AUTHS];
+ 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 = (long)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 && "p".equals(key)) {
+ sb.append("photos uploaded from client: ").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 = (long)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;
+ }
+} \ No newline at end of file
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
new file mode 100644
index 0000000..36d90ce
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
@@ -0,0 +1,177 @@
+/*
+ * 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];
+ mTimePeriodMap = new HashMap[NUM_AUTHS + 1];
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java
new file mode 100644
index 0000000..9ce7045
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java
@@ -0,0 +1,219 @@
+/*
+ * 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 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();
+ mTooltips = new List[NUM_SERIES];
+
+ 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 = (long) 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 = (long) 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 (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/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java
new file mode 100644
index 0000000..2223a4d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java
@@ -0,0 +1,971 @@
+/*
+ * 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) {
+ new InvalidParameterException();
+ }
+
+ mPidFilterList = pids;
+ }
+
+ ArrayList<Integer> getPidFilterList() {
+ return mPidFilterList;
+ }
+
+ void addPidFiler(int pid) {
+ if (mPidFiltering == false) {
+ 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() {
+ 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() {
+ 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java
new file mode 100644
index 0000000..88c3cb2
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java
@@ -0,0 +1,955 @@
+/*
+ * 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.IImageLoader;
+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 IImageLoader mImageLoader;
+
+ 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(IImageLoader imageLoader, Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ mImageLoader = imageLoader;
+ }
+
+ /**
+ * 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() {
+ 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;
+
+ mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayNewButton.setImage(mImageLoader.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(mImageLoader.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(mImageLoader.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(mImageLoader.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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java
new file mode 100644
index 0000000..a1303f6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java
@@ -0,0 +1,82 @@
+/*
+ * 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) {
+ }
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java
new file mode 100644
index 0000000..2621c6a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java
@@ -0,0 +1,926 @@
+/*
+ * 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.Device;
+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.IImageLoader;
+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 IImageLoader mImageLoader;
+
+ private Device 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(IImageLoader imageLoader) {
+ super();
+ mImageLoader = imageLoader;
+ 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() {
+ public void run() {
+ openOptionPanel();
+ }
+ });
+
+ mClearAction = clearAction;
+ mClearAction.setRunnable(new Runnable() {
+ public void run() {
+ clearLog();
+ }
+ });
+
+ mSaveAction = saveAction;
+ mSaveAction.setRunnable(new Runnable() {
+ 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() {
+ 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() {
+ 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(mImageLoader, 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)
+ */
+ 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() {
+ 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 Device 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() {
+ 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)
+ */
+ @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() {
+ 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)
+ */
+ 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() {
+ 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.
+ }
+ }
+
+ @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);
+
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ logReceiver.parseNewData(buffer, 0, count);
+ }
+ }
+
+ @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/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java
new file mode 100644
index 0000000..dd32e2c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java
@@ -0,0 +1,628 @@
+/*
+ * 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.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+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() {
+ 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() {
+ 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/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java
new file mode 100644
index 0000000..3af1447
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java
new file mode 100644
index 0000000..108c097
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java
@@ -0,0 +1,158 @@
+/*
+ * 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;
+
+ 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);
+ }
+ }
+ }
+ } 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)) {
+ return CALENDAR;
+ } else if ("contacts".equals(authname) || "cp".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/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java
new file mode 100644
index 0000000..c66fe48
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.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.ddmuilib.logcat;
+
+import com.android.ddmuilib.IImageLoader;
+
+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 = 250;
+
+ private Shell mParent;
+
+ private Shell mShell;
+
+ private boolean mOk = false;
+
+ private IImageLoader mImageLoader;
+
+ /**
+ * 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 mPidWarning;
+
+ public EditFilterDialog(IImageLoader imageLoader, Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ mImageLoader = imageLoader;
+ }
+
+ public EditFilterDialog(IImageLoader imageLoader, Shell shell,
+ LogFilter filter) {
+ this(imageLoader, 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() {
+ 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(2, 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() {
+ public void modifyText(ModifyEvent e) {
+ mName = filterNameText.getText().trim();
+ validate();
+ }
+ });
+
+ // 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);
+ }
+ }
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ tagText.setLayoutData(gd);
+ tagText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mTag = tagText.getText().trim();
+ validate();
+ }
+ });
+
+ 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() {
+ public void modifyText(ModifyEvent e) {
+ mPid = pidText.getText().trim();
+ validate();
+ }
+ });
+
+ mPidWarning = new Label(main, SWT.NONE);
+ mPidWarning.setImage(mImageLoader.loadImage("empty.png", // $NON-NLS-1$
+ 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);
+ 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() {
+
+ // then we check it only contains digits.
+ if (mPid != null) {
+ if (mPid.matches("[0-9]*") == false) { // $NON-NLS-1$
+ mOkButton.setEnabled(false);
+ mPidWarning.setImage(mImageLoader.loadImage(
+ "warning.png", // $NON-NLS-1$
+ mShell.getDisplay()));
+ return;
+ } else {
+ mPidWarning.setImage(mImageLoader.loadImage(
+ "empty.png", // $NON-NLS-1$
+ mShell.getDisplay()));
+ }
+ }
+
+ if (mName == null || mName.length() == 0) {
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ mOkButton.setEnabled(true);
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java
new file mode 100644
index 0000000..9cff656
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/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/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java
new file mode 100644
index 0000000..a32de2f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java
@@ -0,0 +1,555 @@
+/*
+ * 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);
+ }
+
+ 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/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java
new file mode 100644
index 0000000..bd8b75c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java
@@ -0,0 +1,1571 @@
+/*
+ * 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.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.IImageLoader;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+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 &lt;pid&gt;:0x&lt;???&gt; &lt;severity&gt;/&lt;tag&gt;]"</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 Device 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;
+
+ /** loader for the images. the implementation will varie between standalone
+ * app and eclipse plugin app and eclipse plugin. */
+ private IImageLoader mImageLoader;
+
+ private String mDefaultLogSave;
+
+ private int mColumnMode = COLUMN_MODE_MANUAL;
+ private Font mDisplayFont;
+
+ private ITableFocusListener mGlobalListener;
+
+ /** 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);
+ }
+ }
+
+ 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;
+ }
+
+ 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() {
+ public void run() {
+ mTabItem.setText(name);
+ }
+ });
+
+ // we're done with this ps.
+ mDone = true;
+ return;
+ }
+ }
+ }
+
+ }
+
+
+ /**
+ * Create the log view with some default parameters
+ * @param imageLoader the image loader.
+ * @param colors The display color object
+ * @param filterStorage the storage for user defined filters.
+ * @param mode The filtering mode
+ */
+ public LogPanel(IImageLoader imageLoader, LogColors colors,
+ ILogFilterStorageManager filterStorage, int mode) {
+ mImageLoader = imageLoader;
+ 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() {
+ 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 Device 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); //$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(mImageLoader,
+ 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(mImageLoader,
+ 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.
+ try {
+ FileWriter 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;
+ }
+ }
+ }
+
+ 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() {
+ public void copy(Clipboard clipboard) {
+ copyTable(clipboard, table);
+ }
+
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ 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);
+
+ 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() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ 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() {
+ 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() {
+ 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() {
+ 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;
+
+ // check we aren't overwriting start
+ if (mBufferEnd == mBufferStart) {
+ mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
+ }
+
+ // increment the next usable slot index
+ mBufferEnd = (mBufferEnd + 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) {
+ // hmm...
+ }
+ }
+ }.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());
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/resources/images/add.png b/ddms/libs/ddmuilib/src/resources/images/add.png
new file mode 100644
index 0000000..eefc2ca
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/add.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/android.png b/ddms/libs/ddmuilib/src/resources/images/android.png
new file mode 100644
index 0000000..3779d4d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/android.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/backward.png b/ddms/libs/ddmuilib/src/resources/images/backward.png
new file mode 100644
index 0000000..90a9713
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/backward.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/clear.png b/ddms/libs/ddmuilib/src/resources/images/clear.png
new file mode 100644
index 0000000..0009cf6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/clear.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/d.png b/ddms/libs/ddmuilib/src/resources/images/d.png
new file mode 100644
index 0000000..d45506e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/d.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-attach.png b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png
new file mode 100644
index 0000000..9b8a11c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-error.png b/ddms/libs/ddmuilib/src/resources/images/debug-error.png
new file mode 100644
index 0000000..f22da1f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-error.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-wait.png b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png
new file mode 100644
index 0000000..322be63
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/delete.png b/ddms/libs/ddmuilib/src/resources/images/delete.png
new file mode 100644
index 0000000..db5fab8
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/delete.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/device.png b/ddms/libs/ddmuilib/src/resources/images/device.png
new file mode 100644
index 0000000..7dbbbb6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/device.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/down.png b/ddms/libs/ddmuilib/src/resources/images/down.png
new file mode 100644
index 0000000..f9426cb
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/down.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/e.png b/ddms/libs/ddmuilib/src/resources/images/e.png
new file mode 100644
index 0000000..dee7c97
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/e.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/edit.png b/ddms/libs/ddmuilib/src/resources/images/edit.png
new file mode 100644
index 0000000..b8f65bc
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/edit.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/empty.png b/ddms/libs/ddmuilib/src/resources/images/empty.png
new file mode 100644
index 0000000..f021542
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/empty.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/emulator.png b/ddms/libs/ddmuilib/src/resources/images/emulator.png
new file mode 100644
index 0000000..a718042
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/emulator.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/file.png b/ddms/libs/ddmuilib/src/resources/images/file.png
new file mode 100644
index 0000000..043a814
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/file.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/folder.png b/ddms/libs/ddmuilib/src/resources/images/folder.png
new file mode 100644
index 0000000..7e29b1a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/folder.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/forward.png b/ddms/libs/ddmuilib/src/resources/images/forward.png
new file mode 100644
index 0000000..a97a605
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/forward.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/gc.png b/ddms/libs/ddmuilib/src/resources/images/gc.png
new file mode 100644
index 0000000..5194806
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/gc.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/halt.png b/ddms/libs/ddmuilib/src/resources/images/halt.png
new file mode 100644
index 0000000..10e3720
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/halt.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/heap.png b/ddms/libs/ddmuilib/src/resources/images/heap.png
new file mode 100644
index 0000000..e3aa3f0
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/heap.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/i.png b/ddms/libs/ddmuilib/src/resources/images/i.png
new file mode 100644
index 0000000..98385c5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/i.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/importBug.png b/ddms/libs/ddmuilib/src/resources/images/importBug.png
new file mode 100644
index 0000000..f5da179
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/importBug.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/load.png b/ddms/libs/ddmuilib/src/resources/images/load.png
new file mode 100644
index 0000000..9e7bf6e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/load.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/pause.png b/ddms/libs/ddmuilib/src/resources/images/pause.png
new file mode 100644
index 0000000..19d286d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/pause.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/play.png b/ddms/libs/ddmuilib/src/resources/images/play.png
new file mode 100644
index 0000000..d54f013
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/play.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/pull.png b/ddms/libs/ddmuilib/src/resources/images/pull.png
new file mode 100644
index 0000000..f48f1b1
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/pull.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/push.png b/ddms/libs/ddmuilib/src/resources/images/push.png
new file mode 100644
index 0000000..6222864
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/push.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/save.png b/ddms/libs/ddmuilib/src/resources/images/save.png
new file mode 100644
index 0000000..040ebda
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/save.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/thread.png b/ddms/libs/ddmuilib/src/resources/images/thread.png
new file mode 100644
index 0000000..ac839e8
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/thread.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/up.png b/ddms/libs/ddmuilib/src/resources/images/up.png
new file mode 100644
index 0000000..92edf5a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/up.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/v.png b/ddms/libs/ddmuilib/src/resources/images/v.png
new file mode 100644
index 0000000..8044051
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/v.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/w.png b/ddms/libs/ddmuilib/src/resources/images/w.png
new file mode 100644
index 0000000..129d0f9
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/w.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/warning.png b/ddms/libs/ddmuilib/src/resources/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/warning.png
Binary files differ