summaryrefslogtreecommitdiff
path: root/ddms
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2013-03-07 13:40:07 -0800
committerRaphael Moll <ralf@android.com>2013-03-13 14:33:50 -0700
commit8029da5b58decfc7669ad6d849271fbbd82700b7 (patch)
treef2d44bb8472d0bfc5341c0653c48704acb269a70 /ddms
parent9198deb60d5a57da9c78c98f9ad8f14d83e28ee9 (diff)
downloadswt-8029da5b58decfc7669ad6d849271fbbd82700b7.tar.gz
Move ddms + ddmuilib from sdk.git to tools/swt.
Change-Id: I2093d1d780ff23368abbc18466510c6ad61b6e46
Diffstat (limited to 'ddms')
-rw-r--r--ddms/app/.classpath16
-rw-r--r--ddms/app/.project17
-rw-r--r--ddms/app/.settings/org.eclipse.jdt.core.prefs98
-rw-r--r--ddms/app/NOTICE190
-rw-r--r--ddms/app/README75
-rw-r--r--ddms/app/build.gradle21
-rwxr-xr-xddms/app/etc/ddms111
-rwxr-xr-xddms/app/etc/ddms.bat74
-rw-r--r--ddms/app/src/main/java/com/android/ddms/AboutDialog.java158
-rw-r--r--ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java164
-rw-r--r--ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java441
-rw-r--r--ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java80
-rw-r--r--ddms/app/src/main/java/com/android/ddms/Main.java171
-rw-r--r--ddms/app/src/main/java/com/android/ddms/PrefsDialog.java610
-rw-r--r--ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java395
-rw-r--r--ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java334
-rw-r--r--ddms/app/src/main/java/com/android/ddms/UIThread.java1803
-rw-r--r--ddms/app/src/main/java/images/ddms-128.pngbin0 -> 17692 bytes
-rw-r--r--ddms/ddmuilib/.classpath17
-rw-r--r--ddms/ddmuilib/.project17
-rw-r--r--ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs98
-rw-r--r--ddms/ddmuilib/NOTICE190
-rw-r--r--ddms/ddmuilib/README14
-rw-r--r--ddms/ddmuilib/build.gradle14
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java117
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java355
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java651
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java50
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java193
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java33
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java79
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java784
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java1463
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java142
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java1310
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java21
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java38
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java206
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java199
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java1648
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java49
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java73
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java350
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java78
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java223
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java100
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java60
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java907
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java209
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java132
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java573
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java42
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java71
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java31
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java31
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java91
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java47
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java177
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java922
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java160
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java184
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java195
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java222
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java65
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java112
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java1150
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java90
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java92
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java133
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java135
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java56
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java71
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java306
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java249
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java373
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java210
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java53
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java48
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java87
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java34
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java42
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java46
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java79
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java96
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java55
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java422
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java381
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java304
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java181
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java227
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java975
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java961
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java95
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java938
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java630
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java90
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java173
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java397
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java33
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java26
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java46
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java81
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java63
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java327
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java211
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java116
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java1607
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java151
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java95
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java81
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java27
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java556
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java1626
-rw-r--r--ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java1125
-rw-r--r--ddms/ddmuilib/src/main/java/images/add.pngbin0 -> 146 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/android.pngbin0 -> 3609 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/backward.pngbin0 -> 136 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/capture.pngbin0 -> 691 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/clear.pngbin0 -> 217 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/d.pngbin0 -> 638 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/debug-attach.pngbin0 -> 156 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/debug-error.pngbin0 -> 222 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/debug-wait.pngbin0 -> 156 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/delete.pngbin0 -> 107 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/device.pngbin0 -> 135 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/diff.pngbin0 -> 213 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/displayfilters.pngbin0 -> 242 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/down.pngbin0 -> 141 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/e.pngbin0 -> 511 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/edit.pngbin0 -> 223 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/empty.pngbin0 -> 75 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/emulator.pngbin0 -> 287 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/file.pngbin0 -> 157 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/folder.pngbin0 -> 123 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/forward.pngbin0 -> 137 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/gc.pngbin0 -> 165 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/groupby.pngbin0 -> 413 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/halt.pngbin0 -> 197 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/heap.pngbin0 -> 222 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/hprof.pngbin0 -> 317 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/i.pngbin0 -> 498 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/importBug.pngbin0 -> 191 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/load.pngbin0 -> 163 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/pause.pngbin0 -> 98 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/play.pngbin0 -> 138 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/pull.pngbin0 -> 329 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/push.pngbin0 -> 228 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/save.pngbin0 -> 240 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/scroll_lock.pngbin0 -> 291 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/sort_down.pngbin0 -> 102 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/sort_up.pngbin0 -> 105 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/thread.pngbin0 -> 121 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/tracing_start.pngbin0 -> 227 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/tracing_stop.pngbin0 -> 217 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/up.pngbin0 -> 134 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/v.pngbin0 -> 587 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/w.pngbin0 -> 681 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/warning.pngbin0 -> 147 bytes
-rw-r--r--ddms/ddmuilib/src/main/java/images/zygote.pngbin0 -> 345 bytes
-rw-r--r--ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java188
-rw-r--r--ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java74
-rw-r--r--ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java75
-rw-r--r--ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java54
-rw-r--r--ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java108
164 files changed, 33350 insertions, 0 deletions
diff --git a/ddms/app/.classpath b/ddms/app/.classpath
new file mode 100644
index 0000000..50b7a68
--- /dev/null
+++ b/ddms/app/.classpath
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src/main/java"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmuilib"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/sdkstats"/>
+ <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.core.commands_3.6.0.I20100512-1500.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.equinox.common_3.6.0.v20100503.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.jface_3.6.2.M20110210-1200.jar"/>
+ <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swtmenubar.jar" sourcepath="/ANDROID_SRC/sdk/swtmenubar/src"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/osgi/osgi.jar"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/common"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/app/.project b/ddms/app/.project
new file mode 100644
index 0000000..ffb19d7
--- /dev/null
+++ b/ddms/app/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ddms</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ddms/app/.settings/org.eclipse.jdt.core.prefs b/ddms/app/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/ddms/app/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/ddms/app/NOTICE b/ddms/app/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/ddms/app/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/ddms/app/README b/ddms/app/README
new file mode 100644
index 0000000..0d9bbc4
--- /dev/null
+++ b/ddms/app/README
@@ -0,0 +1,75 @@
+Using the Eclipse project DDMS
+------------------------------
+
+DDMS requires some external libraries to compile.
+If you build DDMS using the makefile, you have nothing to configure.
+However if you want to develop on DDMS using Eclipse, you need to
+perform the following configuration.
+
+
+-------
+1- Projects required in Eclipse
+-------
+
+To run DDMS from Eclipse, you need to import the following 5 projects:
+
+ - sdk/androidpprefs: project AndroidPrefs
+ - sdk/sdkstats: project SdkStatsService
+ - sdk/ddms/app: project Ddms
+ - sdk/ddms/libs/ddmlib: project Ddmlib
+ - sdk/ddms/libs/ddmuilib: project Ddmuilib
+
+
+-------
+2- DDMS requires some SWT and OSGI JARs to compile.
+-------
+
+SWT is available in the tree under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside
+the project directory, the .classpath file references a user library
+called ANDROID_SWT.
+SWT depends on OSGI, so we'll also create an ANDROID_OSGI library for that.
+
+In order to compile the project:
+- Open Preferences > Java > Build Path > User Libraries
+
+- Create a new user library named ANDROID_SWT
+- Add the following 4 JAR files:
+
+ - prebuilt/<platform>/swt/swt.jar
+ - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar
+ - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar
+ - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar
+
+- Create a new user library named ANDROID_OSGI
+- Add the following JAR file:
+
+ - prebuilt/common/eclipse/org.eclipse.osgi_3.*.jar
+
+
+-------
+3- DDMS also requires the compiled SwtMenuBar library.
+-------
+
+Build the swtmenubar library:
+$ cd $TOP (top of Android tree)
+$ . build/envsetup.sh && lunch sdk-eng
+$ sdk/eclipse/scripts/create_sdkman_symlinks.sh
+
+Define a classpath variable in Eclipse:
+- Open Preferences > Java > Build Path > Classpath Variables
+- Create a new classpath variable named ANDROID_OUT_FRAMEWORK
+- Set its folder value to <Android tree>/out/host/<platform>/framework
+- Create a new classpath variable named ANDROID_SRC
+- Set its folder value to <Android tree>
+
+You might need to clean the ddms project (Project > Clean...) after
+you add the new classpath variable, otherwise previous errors might not
+go away automatically.
+
+The ANDROID_SRC part should be optional. It allows you to have access to
+the SwtMenuBar generic parts from the Java editor.
+
+--
+EOF
diff --git a/ddms/app/build.gradle b/ddms/app/build.gradle
new file mode 100644
index 0000000..a07e3bd
--- /dev/null
+++ b/ddms/app/build.gradle
@@ -0,0 +1,21 @@
+group = 'com.android.tools.ddms'
+archivesBaseName = 'ddms'
+
+dependencies {
+ compile project(':ddmuilib')
+ compile project(':sdkstats')
+ compile project(':swtmenubar')
+ compile "com.android.tools:common:$version"
+ compile "com.android.tools.ddms:ddmlib:$version"
+}
+
+shipping {
+ launcherScripts = ['etc/ddms', 'etc/ddms.bat']
+}
+
+// include swt for compilation
+sourceSets.main.compileClasspath += configurations.swt
+
+// configure the manifest of the buildDistributionJar task.
+buildDistributionJar.manifest.attributes("Main-Class": "com.android.ddms.Main")
+
diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms
new file mode 100755
index 0000000..79b93f9
--- /dev/null
+++ b/ddms/app/etc/ddms
@@ -0,0 +1,111 @@
+#!/bin/bash
+# Copyright 2005-2007, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+ newProg=`/bin/ls -ld "${prog}"`
+ newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+ if expr "x${newProg}" : 'x/' >/dev/null; then
+ prog="${newProg}"
+ else
+ progdir=`dirname "${prog}"`
+ prog="${progdir}/${newProg}"
+ fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=ddms.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/tools/lib
+ libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/framework
+ libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ echo `basename "$prog"`": can't find $jarfile"
+ exit 1
+fi
+
+
+# Check args.
+if [ debug = "$1" ]; then
+ # add this in for debugging
+ java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+ shift 1
+else
+ java_debug=
+fi
+
+javaCmd="java"
+
+# Mac OS X needs an additional arg, or you get an "illegal thread" complaint.
+if [ `uname` = "Darwin" ]; then
+ os_opts="-XstartOnFirstThread"
+else
+ os_opts=
+fi
+
+if [ `uname` = "Linux" ]; then
+ export GDK_NATIVE_WINDOWS=true
+fi
+
+jarpath="$frameworkdir/$jarfile:$frameworkdir/swtmenubar.jar"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+ swtpath="$ANDROID_SWT"
+else
+ vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+ if [ -n "$ANDROID_BUILD_TOP" ]; then
+ osname=`uname -s | tr A-Z a-z`
+ swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+ else
+ swtpath="${frameworkdir}/${vmarch}"
+ fi
+fi
+
+if [ ! -d "$swtpath" ]; then
+ echo "SWT folder '${swtpath}' does not exist."
+ echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+ exit 1
+fi
+
+if [ -x $progdir/monitor ]; then
+ echo "The standalone version of DDMS is deprecated."
+ echo "Please use Android Device Monitor (tools/monitor) instead."
+fi
+exec "$javaCmd" \
+ -Xmx256M $os_opts $java_debug \
+ -Dcom.android.ddms.bindir="$progdir" \
+ -classpath "$jarpath:$swtpath/swt.jar" \
+ com.android.ddms.Main "$@"
diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat
new file mode 100755
index 0000000..d710ea6
--- /dev/null
+++ b/ddms/app/etc/ddms.bat
@@ -0,0 +1,74 @@
+@echo off
+rem Copyright (C) 2007 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory and drive to where the script is, to avoid
+rem issues with directories containing whitespaces.
+cd /d %~dp0
+
+rem Get the CWD as a full path with short names only (without spaces)
+for %%i in ("%cd%") do set prog_dir=%%~fsi
+
+rem Check we have a valid Java.exe in the path.
+set java_exe=
+call lib\find_java.bat
+if not defined java_exe goto :EOF
+
+set jarfile=ddms.jar
+set frameworkdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=..\framework\
+
+:JarFileOk
+
+if debug NEQ "%1" goto NoDebug
+ set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+ shift 1
+:NoDebug
+
+set jarpath=%frameworkdir%%jarfile%;%frameworkdir%swtmenubar.jar
+
+if not defined ANDROID_SWT goto QueryArch
+ set swt_path=%ANDROID_SWT%
+ goto SwtDone
+
+:QueryArch
+
+ for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a
+
+:SwtDone
+
+if exist %swt_path% goto SetPath
+ echo SWT folder '%swt_path%' does not exist.
+ echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
+ exit /B
+
+:SetPath
+set javaextdirs=%swt_path%;%frameworkdir%
+
+echo The standalone version of DDMS is deprecated.
+echo Please use Android Device Monitor (monitor.bat) instead.
+call %java_exe% %java_debug% -Dcom.android.ddms.bindir=%prog_dir% -classpath "%jarpath%;%swt_path%\swt.jar" com.android.ddms.Main %*
+
diff --git a/ddms/app/src/main/java/com/android/ddms/AboutDialog.java b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java
new file mode 100644
index 0000000..b3ddff7
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java
@@ -0,0 +1,158 @@
+/* //device/tools/ddms/src/com/android/ddms/AboutDialog.java
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package com.android.ddms;
+
+import com.android.ddmlib.Log;
+import com.android.ddmuilib.ImageLoader;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.InputStream;
+
+/**
+ * Our "about" box.
+ */
+public class AboutDialog extends Dialog {
+
+ private Image logoImage;
+
+ /**
+ * Create with default style.
+ */
+ public AboutDialog(Shell parent) {
+ this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Create with app-defined style.
+ */
+ public AboutDialog(Shell parent, int style) {
+ super(parent, style);
+ }
+
+ /**
+ * Prepare and display the dialog.
+ */
+ public void open() {
+ Shell parent = getParent();
+ Shell shell = new Shell(parent, getStyle());
+ shell.setText("About...");
+
+ logoImage = loadImage(shell, "ddms-128.png"); //$NON-NLS-1$
+ createContents(shell);
+ shell.pack();
+
+ shell.open();
+ Display display = parent.getDisplay();
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ logoImage.dispose();
+ }
+
+ /*
+ * Load an image file from a resource.
+ *
+ * This depends on Display, so I'm not sure what the rules are for
+ * loading once and caching in a static class field.
+ */
+ private Image loadImage(Shell shell, String fileName) {
+ InputStream imageStream;
+ String pathName = "/images/" + fileName; //$NON-NLS-1$
+
+ imageStream = this.getClass().getResourceAsStream(pathName);
+ if (imageStream == null) {
+ //throw new NullPointerException("couldn't find " + pathName);
+ Log.w("ddms", "Couldn't load " + pathName);
+ Display display = shell.getDisplay();
+ return ImageLoader.createPlaceHolderArt(display, 100, 50,
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+
+ Image img = new Image(shell.getDisplay(), imageStream);
+ if (img == null)
+ throw new NullPointerException("couldn't load " + pathName);
+ return img;
+ }
+
+ /*
+ * Create the about box contents.
+ */
+ private void createContents(final Shell shell) {
+ GridLayout layout;
+ GridData data;
+ Label label;
+
+ shell.setLayout(new GridLayout(2, false));
+
+ // Fancy logo
+ Label logo = new Label(shell, SWT.BORDER);
+ logo.setImage(logoImage);
+
+ // Text Area
+ Composite textArea = new Composite(shell, SWT.NONE);
+ layout = new GridLayout(1, true);
+ textArea.setLayout(layout);
+
+ // Text lines
+ label = new Label(textArea, SWT.NONE);
+ if (Main.sRevision != null && Main.sRevision.length() > 0) {
+ label.setText("Dalvik Debug Monitor Revision " + Main.sRevision);
+ } else {
+ label.setText("Dalvik Debug Monitor");
+ }
+ label = new Label(textArea, SWT.NONE);
+ // TODO: update with new year date (search this to find other occurrences to update)
+ label.setText("Copyright 2007-2012, The Android Open Source Project");
+ label = new Label(textArea, SWT.NONE);
+ label.setText("All Rights Reserved.");
+
+ // blank spot in grid
+ label = new Label(shell, SWT.NONE);
+
+ // "OK" button
+ Button ok = new Button(shell, SWT.PUSH);
+ ok.setText("OK");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_END);
+ data.widthHint = 80;
+ ok.setLayoutData(data);
+ ok.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ shell.close();
+ }
+ });
+
+ shell.pack();
+
+ shell.setDefaultButton(ok);
+ }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java
new file mode 100644
index 0000000..2dcd5d4
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddms;
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * DDMS implementation of the IDebugPortProvider interface.
+ * This class handles saving/loading the list of static debug port from
+ * the preference store and provides the port number to the Device Monitor.
+ */
+public class DebugPortProvider implements IDebugPortProvider {
+
+ private static DebugPortProvider sThis = new DebugPortProvider();
+
+ /** Preference name for the static port list. */
+ public static final String PREFS_STATIC_PORT_LIST = "android.staticPortList"; //$NON-NLS-1$
+
+ /**
+ * Mapping device serial numbers to maps. The embedded maps are mapping application names to
+ * debugger ports.
+ */
+ private Map<String, Map<String, Integer>> mMap;
+
+ public static DebugPortProvider getInstance() {
+ return sThis;
+ }
+
+ private DebugPortProvider() {
+ computePortList();
+ }
+
+ /**
+ * Returns a static debug port for the specified application running on the
+ * specified {@link IDevice}.
+ * @param device The device the application is running on.
+ * @param appName The application name, as defined in the
+ * AndroidManifest.xml package attribute.
+ * @return The static debug port or {@link #NO_STATIC_PORT} if there is none setup.
+ *
+ * @see IDebugPortProvider#getPort(IDevice, String)
+ */
+ @Override
+ public int getPort(IDevice device, String appName) {
+ if (mMap != null) {
+ Map<String, Integer> deviceMap = mMap.get(device.getSerialNumber());
+ if (deviceMap != null) {
+ Integer i = deviceMap.get(appName);
+ if (i != null) {
+ return i.intValue();
+ }
+ }
+ }
+ return IDebugPortProvider.NO_STATIC_PORT;
+ }
+
+ /**
+ * Returns the map of Static debugger ports. The map links device serial numbers to
+ * a map linking application name to debugger ports.
+ */
+ public Map<String, Map<String, Integer>> getPortList() {
+ return mMap;
+ }
+
+ /**
+ * Create the map member from the values contained in the Preference Store.
+ */
+ private void computePortList() {
+ mMap = new HashMap<String, Map<String, Integer>>();
+
+ // get the prefs store
+ IPreferenceStore store = PrefsDialog.getStore();
+ String value = store.getString(PREFS_STATIC_PORT_LIST);
+
+ if (value != null && value.length() > 0) {
+ // format is
+ // port1|port2|port3|...
+ // where port# is
+ // appPackageName:appPortNumber:device-serial-number
+ String[] portSegments = value.split("\\|"); //$NON-NLS-1$
+ for (String seg : portSegments) {
+ String[] entry = seg.split(":"); //$NON-NLS-1$
+
+ // backward compatibility support. if we have only 2 entry, we default
+ // to the first emulator.
+ String deviceName = null;
+ if (entry.length == 3) {
+ deviceName = entry[2];
+ } else {
+ deviceName = IDevice.FIRST_EMULATOR_SN;
+ }
+
+ // get the device map
+ Map<String, Integer> deviceMap = mMap.get(deviceName);
+ if (deviceMap == null) {
+ deviceMap = new HashMap<String, Integer>();
+ mMap.put(deviceName, deviceMap);
+ }
+
+ deviceMap.put(entry[0], Integer.valueOf(entry[1]));
+ }
+ }
+ }
+
+ /**
+ * Sets new [device, app, port] values.
+ * The values are also sync'ed in the preference store.
+ * @param map The map containing the new values.
+ */
+ public void setPortList(Map<String, Map<String,Integer>> map) {
+ // update the member map.
+ mMap.clear();
+ mMap.putAll(map);
+
+ // create the value to store in the preference store.
+ // see format definition in getPortList
+ StringBuilder sb = new StringBuilder();
+
+ Set<String> deviceKeys = map.keySet();
+ for (String deviceKey : deviceKeys) {
+ Map<String, Integer> deviceMap = map.get(deviceKey);
+ if (deviceMap != null) {
+ Set<String> appKeys = deviceMap.keySet();
+
+ for (String appKey : appKeys) {
+ Integer port = deviceMap.get(appKey);
+ if (port != null) {
+ sb.append(appKey).append(':').append(port.intValue()).append(':').
+ append(deviceKey).append('|');
+ }
+ }
+ }
+ }
+
+ String value = sb.toString();
+
+ // get the prefs store.
+ IPreferenceStore store = PrefsDialog.getStore();
+
+ // and give it the new value.
+ store.setValue(PREFS_STATIC_PORT_LIST, value);
+ }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java
new file mode 100644
index 0000000..6775cbb
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java
@@ -0,0 +1,441 @@
+/* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package com.android.ddms;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+
+/**
+ * Execute a command on an ADB-attached device and save the output.
+ *
+ * There are several ways to do this. One is to run a single command
+ * and show the output. Another is to have several possible commands and
+ * let the user click a button next to the one (or ones) they want. This
+ * currently uses the simple 1:1 form.
+ */
+public class DeviceCommandDialog extends Dialog {
+
+ public static final int DEVICE_STATE = 0;
+ public static final int APP_STATE = 1;
+ public static final int RADIO_STATE = 2;
+ public static final int LOGCAT = 3;
+
+ private String mCommand;
+ private String mFileName;
+
+ private Label mStatusLabel;
+ private Button mCancelDone;
+ private Button mSave;
+ private Text mText;
+ private Font mFont = null;
+ private boolean mCancel;
+ private boolean mFinished;
+
+
+ /**
+ * Create with default style.
+ */
+ public DeviceCommandDialog(String command, String fileName, Shell parent) {
+ // don't want a close button, but it seems hard to get rid of on GTK
+ // keep it on all platforms for consistency
+ this(command, fileName, parent,
+ SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE);
+ }
+
+ /**
+ * Create with app-defined style.
+ */
+ public DeviceCommandDialog(String command, String fileName, Shell parent,
+ int style)
+ {
+ super(parent, style);
+ mCommand = command;
+ mFileName = fileName;
+ }
+
+ /**
+ * Prepare and display the dialog.
+ * @param currentDevice
+ */
+ public void open(IDevice currentDevice) {
+ Shell parent = getParent();
+ Shell shell = new Shell(parent, getStyle());
+ shell.setText("Remote Command");
+
+ mFinished = false;
+ mFont = findFont(shell.getDisplay());
+ createContents(shell);
+
+ // Getting weird layout behavior under Linux when Text is added --
+ // looks like text widget has min width of 400 when FILL_HORIZONTAL
+ // is used, and layout gets tweaked to force this. (Might be even
+ // more with the scroll bars in place -- it wigged out when the
+ // file save dialog was invoked.)
+ shell.setMinimumSize(500, 200);
+ shell.setSize(800, 600);
+ shell.open();
+
+ executeCommand(shell, currentDevice);
+
+ Display display = parent.getDisplay();
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ if (mFont != null)
+ mFont.dispose();
+ }
+
+ /*
+ * Create a text widget to show the output and some buttons to
+ * manage things.
+ */
+ private void createContents(final Shell shell) {
+ GridData data;
+
+ shell.setLayout(new GridLayout(2, true));
+
+ shell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ if (!mFinished) {
+ Log.d("ddms", "NOT closing - cancelling command");
+ event.doit = false;
+ mCancel = true;
+ }
+ }
+ });
+
+ mStatusLabel = new Label(shell, SWT.NONE);
+ mStatusLabel.setText("Executing '" + shortCommandString() + "'");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+ data.horizontalSpan = 2;
+ mStatusLabel.setLayoutData(data);
+
+ mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+ mText.setEditable(false);
+ mText.setFont(mFont);
+ data = new GridData(GridData.FILL_BOTH);
+ data.horizontalSpan = 2;
+ mText.setLayoutData(data);
+
+ // "save" button
+ mSave = new Button(shell, SWT.PUSH);
+ mSave.setText("Save");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ mSave.setLayoutData(data);
+ mSave.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ saveText(shell);
+ }
+ });
+ mSave.setEnabled(false);
+
+ // "cancel/done" button
+ mCancelDone = new Button(shell, SWT.PUSH);
+ mCancelDone.setText("Cancel");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ mCancelDone.setLayoutData(data);
+ mCancelDone.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (!mFinished)
+ mCancel = true;
+ else
+ shell.close();
+ }
+ });
+ }
+
+ /*
+ * Figure out what font to use.
+ *
+ * Returns "null" if we can't figure it out, which SWT understands to
+ * mean "use default system font".
+ */
+ private Font findFont(Display display) {
+ String fontStr = PrefsDialog.getStore().getString("textOutputFont");
+ if (fontStr != null) {
+ FontData fdat = new FontData(fontStr);
+ if (fdat != null)
+ return new Font(display, fdat);
+ }
+ return null;
+ }
+
+
+ /*
+ * Callback class for command execution.
+ */
+ class Gatherer extends Thread implements IShellOutputReceiver {
+ public static final int RESULT_UNKNOWN = 0;
+ public static final int RESULT_SUCCESS = 1;
+ public static final int RESULT_FAILURE = 2;
+ public static final int RESULT_CANCELLED = 3;
+
+ private Shell mShell;
+ private String mCommand;
+ private Text mText;
+ private int mResult;
+ private IDevice mDevice;
+
+ /**
+ * Constructor; pass in the text widget that will receive the output.
+ * @param device
+ */
+ public Gatherer(Shell shell, IDevice device, String command, Text text) {
+ mShell = shell;
+ mDevice = device;
+ mCommand = command;
+ mText = text;
+ mResult = RESULT_UNKNOWN;
+
+ // this is in outer class
+ mCancel = false;
+ }
+
+ /**
+ * Thread entry point.
+ */
+ @Override
+ public void run() {
+
+ if (mDevice == null) {
+ Log.w("ddms", "Cannot execute command: no device selected.");
+ mResult = RESULT_FAILURE;
+ } else {
+ try {
+ mDevice.executeShellCommand(mCommand, this);
+ if (mCancel)
+ mResult = RESULT_CANCELLED;
+ else
+ mResult = RESULT_SUCCESS;
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Remote exec failed: " + ioe.getMessage());
+ mResult = RESULT_FAILURE;
+ } catch (TimeoutException e) {
+ Log.w("ddms", "Remote exec failed: " + e.getMessage());
+ mResult = RESULT_FAILURE;
+ } catch (AdbCommandRejectedException e) {
+ Log.w("ddms", "Remote exec failed: " + e.getMessage());
+ mResult = RESULT_FAILURE;
+ } catch (ShellCommandUnresponsiveException e) {
+ Log.w("ddms", "Remote exec failed: " + e.getMessage());
+ mResult = RESULT_FAILURE;
+ }
+ }
+
+ mShell.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateForResult(mResult);
+ }
+ });
+ }
+
+ /**
+ * Called by executeRemoteCommand().
+ */
+ @Override
+ public void addOutput(byte[] data, int offset, int length) {
+
+ Log.v("ddms", "received " + length + " bytes");
+ try {
+ final String text;
+ text = new String(data, offset, length, "ISO-8859-1");
+
+ // add to text widget; must do in UI thread
+ mText.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mText.append(text);
+ }
+ });
+ }
+ catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace(); // not expected
+ }
+ }
+
+ @Override
+ public void flush() {
+ // nothing to flush.
+ }
+
+ /**
+ * Called by executeRemoteCommand().
+ */
+ @Override
+ public boolean isCancelled() {
+ return mCancel;
+ }
+ };
+
+ /*
+ * Execute a remote command, add the output to the text widget, and
+ * update controls.
+ *
+ * We have to run the command in a thread so that the UI continues
+ * to work.
+ */
+ private void executeCommand(Shell shell, IDevice device) {
+ Gatherer gath = new Gatherer(shell, device, commandString(), mText);
+ gath.start();
+ }
+
+ /*
+ * Update the controls after the remote operation completes. This
+ * must be called from the UI thread.
+ */
+ private void updateForResult(int result) {
+ if (result == Gatherer.RESULT_SUCCESS) {
+ mStatusLabel.setText("Successfully executed '"
+ + shortCommandString() + "'");
+ mSave.setEnabled(true);
+ } else if (result == Gatherer.RESULT_CANCELLED) {
+ mStatusLabel.setText("Execution cancelled; partial results below");
+ mSave.setEnabled(true); // save partial
+ } else if (result == Gatherer.RESULT_FAILURE) {
+ mStatusLabel.setText("Failed");
+ }
+ mStatusLabel.pack();
+ mCancelDone.setText("Done");
+ mFinished = true;
+ }
+
+ /*
+ * Allow the user to save the contents of the text dialog.
+ */
+ private void saveText(Shell shell) {
+ FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+ String fileName;
+
+ dlg.setText("Save output...");
+ dlg.setFileName(defaultFileName());
+ dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir"));
+ dlg.setFilterNames(new String[] {
+ "Text Files (*.txt)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.txt"
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ PrefsDialog.getStore().setValue("lastTextSaveDir",
+ dlg.getFilterPath());
+
+ Log.d("ddms", "Saving output to " + fileName);
+
+ /*
+ * Convert to 8-bit characters.
+ */
+ String text = mText.getText();
+ byte[] ascii;
+ try {
+ ascii = text.getBytes("ISO-8859-1");
+ }
+ catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace();
+ ascii = new byte[0];
+ }
+
+ /*
+ * Output data, converting CRLF to LF.
+ */
+ try {
+ int length = ascii.length;
+
+ FileOutputStream outFile = new FileOutputStream(fileName);
+ BufferedOutputStream out = new BufferedOutputStream(outFile);
+ for (int i = 0; i < length; i++) {
+ if (i < length-1 &&
+ ascii[i] == 0x0d && ascii[i+1] == 0x0a)
+ {
+ continue;
+ }
+ out.write(ascii[i]);
+ }
+ out.close(); // flush buffer, close file
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to save " + fileName + ": " + ioe);
+ }
+ }
+ }
+
+
+ /*
+ * Return the shell command we're going to use.
+ */
+ private String commandString() {
+ return mCommand;
+
+ }
+
+ /*
+ * Return a default filename for the "save" command.
+ */
+ private String defaultFileName() {
+ return mFileName;
+ }
+
+ /*
+ * Like commandString(), but length-limited.
+ */
+ private String shortCommandString() {
+ String str = commandString();
+ if (str.length() > 50)
+ return str.substring(0, 50) + "...";
+ else
+ return str;
+ }
+}
+
diff --git a/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java
new file mode 100644
index 0000000..04d921c
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java
@@ -0,0 +1,80 @@
+/* //device/tools/ddms/src/com/android/ddms/DropdownSelectionListener.java
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package com.android.ddms;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+/**
+ * Helper class for drop-down menus in toolbars.
+ */
+public class DropdownSelectionListener extends SelectionAdapter {
+ private Menu mMenu;
+ private ToolItem mDropdown;
+
+ /**
+ * Basic constructor. Creates an empty Menu to hold items.
+ */
+ public DropdownSelectionListener(ToolItem item) {
+ mDropdown = item;
+ mMenu = new Menu(item.getParent().getShell(), SWT.POP_UP);
+ }
+
+ /**
+ * Add an item to the dropdown menu.
+ */
+ public void add(String label) {
+ MenuItem item = new MenuItem(mMenu, SWT.NONE);
+ item.setText(label);
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // update the dropdown's text to match the selection
+ MenuItem sel = (MenuItem) e.widget;
+ mDropdown.setText(sel.getText());
+ }
+ });
+ }
+
+ /**
+ * Invoked when dropdown or neighboring arrow is clicked.
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.detail == SWT.ARROW) {
+ // arrow clicked, show menu
+ ToolItem item = (ToolItem) e.widget;
+ Rectangle rect = item.getBounds();
+ Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y));
+ mMenu.setLocation(pt.x, pt.y + rect.height);
+ mMenu.setVisible(true);
+ } else {
+ // button clicked
+ Log.d("ddms", mDropdown.getText() + " Pressed");
+ }
+ }
+}
+
diff --git a/ddms/app/src/main/java/com/android/ddms/Main.java b/ddms/app/src/main/java/com/android/ddms/Main.java
new file mode 100644
index 0000000..bfdb78b
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/Main.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddms;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.DebugPortManager;
+import com.android.ddmlib.Log;
+import com.android.sdkstats.SdkStatsService;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.RuntimeMXBean;
+import java.util.Properties;
+
+
+/**
+ * Start the UI and network.
+ */
+public class Main {
+
+ public static String sRevision;
+
+ public Main() {
+ }
+
+ /*
+ * If a thread bails with an uncaught exception, bring the whole
+ * thing down.
+ */
+ private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ Log.e("ddms", "shutting down due to uncaught exception");
+ Log.e("ddms", e);
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Parse args, start threads.
+ */
+ public static void main(String[] args) {
+ // In order to have the AWT/SWT bridge work on Leopard, we do this little hack.
+ if (isMac()) {
+ RuntimeMXBean rt = ManagementFactory.getRuntimeMXBean();
+ System.setProperty(
+ "JAVA_STARTED_ON_FIRST_THREAD_" + (rt.getName().split("@"))[0], //$NON-NLS-1$
+ "1"); //$NON-NLS-1$
+ }
+
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
+
+ // load prefs and init the default values
+ PrefsDialog.init();
+
+ Log.d("ddms", "Initializing");
+
+ // Create an initial shell display with the correct app name.
+ Display.setAppName(UIThread.APP_NAME);
+ Shell shell = new Shell(Display.getDefault());
+
+ // if this is the first time using ddms or adt, open up the stats service
+ // opt out dialog, and request user for permissions.
+ SdkStatsService stats = new SdkStatsService();
+ stats.checkUserPermissionForPing(shell);
+
+ // the "ping" argument means to check in with the server and exit
+ // the application name and version number must also be supplied
+ if (args.length >= 3 && args[0].equals("ping")) {
+ stats.ping(args);
+ return;
+ } else if (args.length > 0) {
+ Log.e("ddms", "Unknown argument: " + args[0]);
+ System.exit(1);
+ }
+
+ // get the ddms parent folder location
+ String ddmsParentLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$
+
+ if (ddmsParentLocation == null) {
+ // Tip: for debugging DDMS in eclipse, set this env var to the SDK/tools
+ // directory path.
+ ddmsParentLocation = System.getenv("com.android.ddms.bindir"); //$NON-NLS-1$
+ }
+
+ // we're past the point where ddms can be called just to send a ping, so we can
+ // ping for ddms itself.
+ ping(stats, ddmsParentLocation);
+ stats = null;
+
+ DebugPortManager.setProvider(DebugPortProvider.getInstance());
+
+ // create the three main threads
+ UIThread ui = UIThread.getInstance();
+
+ try {
+ ui.runUI(ddmsParentLocation);
+ } finally {
+ PrefsDialog.save();
+
+ AndroidDebugBridge.terminate();
+ }
+
+ Log.d("ddms", "Bye");
+
+ // this is kinda bad, but on MacOS the shutdown doesn't seem to finish because of
+ // a thread called AWT-Shutdown. This will help while I track this down.
+ System.exit(0);
+ }
+
+ /** Return true iff we're running on a Mac */
+ static boolean isMac() {
+ // TODO: Replace usages of this method with
+ // org.eclipse.jface.util.Util#isMac() when we switch to Eclipse 3.5
+ // (ddms is currently built with SWT 3.4.2 from ANDROID_SWT)
+ return System.getProperty("os.name").startsWith("Mac OS"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ private static void ping(SdkStatsService stats, String ddmsParentLocation) {
+ Properties p = new Properties();
+ try{
+ File sourceProp;
+ if (ddmsParentLocation != null && ddmsParentLocation.length() > 0) {
+ sourceProp = new File(ddmsParentLocation, "source.properties"); //$NON-NLS-1$
+ } else {
+ sourceProp = new File("source.properties"); //$NON-NLS-1$
+ }
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(sourceProp);
+ p.load(fis);
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ sRevision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$
+ if (sRevision != null && sRevision.length() > 0) {
+ stats.ping("ddms", sRevision); //$NON-NLS-1$
+ }
+ } catch (FileNotFoundException e) {
+ // couldn't find the file? don't ping.
+ } catch (IOException e) {
+ // couldn't find the file? don't ping.
+ }
+ }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java
new file mode 100644
index 0000000..acadeb8
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddms;
+
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.PortFieldEditor;
+import com.android.ddmuilib.logcat.LogCatMessageList;
+import com.android.ddmuilib.logcat.LogCatPanel;
+import com.android.sdkstats.DdmsPreferenceStore;
+import com.android.sdkstats.SdkStatsPermissionDialog;
+
+import org.eclipse.jface.preference.BooleanFieldEditor;
+import org.eclipse.jface.preference.DirectoryFieldEditor;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.FontFieldEditor;
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.jface.preference.PreferenceDialog;
+import org.eclipse.jface.preference.PreferenceManager;
+import org.eclipse.jface.preference.PreferenceNode;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.jface.preference.RadioGroupFieldEditor;
+import org.eclipse.jface.preference.StringFieldEditor;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Preferences dialog.
+ */
+public final class PrefsDialog {
+
+ // public const values for storage
+ public final static String SHELL_X = "shellX"; //$NON-NLS-1$
+ public final static String SHELL_Y = "shellY"; //$NON-NLS-1$
+ public final static String SHELL_WIDTH = "shellWidth"; //$NON-NLS-1$
+ public final static String SHELL_HEIGHT = "shellHeight"; //$NON-NLS-1$
+ public final static String EXPLORER_SHELL_X = "explorerShellX"; //$NON-NLS-1$
+ public final static String EXPLORER_SHELL_Y = "explorerShellY"; //$NON-NLS-1$
+ public final static String EXPLORER_SHELL_WIDTH = "explorerShellWidth"; //$NON-NLS-1$
+ public final static String EXPLORER_SHELL_HEIGHT = "explorerShellHeight"; //$NON-NLS-1$
+ public final static String SHOW_NATIVE_HEAP = "native"; //$NON-NLS-1$
+
+ public final static String LOGCAT_COLUMN_MODE = "ddmsLogColumnMode"; //$NON-NLS-1$
+ public final static String LOGCAT_FONT = "ddmsLogFont"; //$NON-NLS-1$
+
+ public final static String LOGCAT_COLUMN_MODE_AUTO = "auto"; //$NON-NLS-1$
+ public final static String LOGCAT_COLUMN_MODE_MANUAL = "manual"; //$NON-NLS-1$
+
+ private final static String PREFS_DEBUG_PORT_BASE = "adbDebugBasePort"; //$NON-NLS-1$
+ private final static String PREFS_SELECTED_DEBUG_PORT = "debugSelectedPort"; //$NON-NLS-1$
+ private final static String PREFS_DEFAULT_THREAD_UPDATE = "defaultThreadUpdateEnabled"; //$NON-NLS-1$
+ private final static String PREFS_DEFAULT_HEAP_UPDATE = "defaultHeapUpdateEnabled"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_REFRESH_INTERVAL = "threadStatusInterval"; //$NON-NLS-1$
+ private final static String PREFS_LOG_LEVEL = "ddmsLogLevel"; //$NON-NLS-1$
+ private final static String PREFS_TIMEOUT = "timeOut"; //$NON-NLS-1$
+ private final static String PREFS_PROFILER_BUFFER_SIZE_MB = "profilerBufferSizeMb"; //$NON-NLS-1$
+ private final static String PREFS_USE_ADBHOST = "useAdbHost"; //$NON-NLS-1$
+ private final static String PREFS_ADBHOST_VALUE = "adbHostValue"; //$NON-NLS-1$
+
+ // Preference store.
+ private static DdmsPreferenceStore mStore = new DdmsPreferenceStore();
+
+ /**
+ * Private constructor -- do not instantiate.
+ */
+ private PrefsDialog() {}
+
+ /**
+ * Return the PreferenceStore that holds our values.
+ *
+ * @deprecated Callers should use {@link DdmsPreferenceStore} directly.
+ */
+ @Deprecated
+ public static PreferenceStore getStore() {
+ return mStore.getPreferenceStore();
+ }
+
+ /**
+ * Save the prefs to the config file.
+ *
+ * @deprecated Callers should use {@link DdmsPreferenceStore} directly.
+ */
+ @Deprecated
+ public static void save() {
+ try {
+ mStore.getPreferenceStore().save();
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage());
+ }
+ }
+
+ /**
+ * Do some one-time prep.
+ *
+ * The original plan was to let the individual classes define their
+ * own defaults, which we would get and then override with the config
+ * file. However, PreferencesStore.load() doesn't trigger the "changed"
+ * events, which means we have to pull the loaded config values out by
+ * hand.
+ *
+ * So, we set the defaults, load the values from the config file, and
+ * then run through and manually export the values. Then we duplicate
+ * the second part later on for the "changed" events.
+ */
+ public static void init() {
+ PreferenceStore prefStore = mStore.getPreferenceStore();
+
+ if (prefStore == null) {
+ // we have a serious issue here...
+ Log.e("ddms",
+ "failed to access both the user HOME directory and the system wide temp folder. Quitting.");
+ System.exit(1);
+ }
+
+ // configure default values
+ setDefaults(System.getProperty("user.home")); //$NON-NLS-1$
+
+ // listen for changes
+ prefStore.addPropertyChangeListener(new ChangeListener());
+
+ // Now we initialize the value of the preference, from the values in the store.
+
+ // First the ddm lib.
+ DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE));
+ DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+ DdmPreferences.setLogLevel(prefStore.getString(PREFS_LOG_LEVEL));
+ DdmPreferences.setInitialThreadUpdate(prefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE));
+ DdmPreferences.setInitialHeapUpdate(prefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE));
+ DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT));
+ DdmPreferences.setProfilerBufferSizeMb(prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB));
+ DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST));
+ DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE));
+
+ // some static values
+ String out = System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$
+ DdmUiPreferences.setSymbolsLocation(out + File.separator + "symbols"); //$NON-NLS-1$
+ DdmUiPreferences.setAddr2LineLocation("arm-linux-androideabi-addr2line"); //$NON-NLS-1$
+
+ String traceview = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$
+ if (traceview != null && traceview.length() != 0) {
+ traceview += File.separator + DdmConstants.FN_TRACEVIEW;
+ } else {
+ traceview = DdmConstants.FN_TRACEVIEW;
+ }
+ DdmUiPreferences.setTraceviewLocation(traceview);
+
+ // Now the ddmui lib
+ DdmUiPreferences.setStore(prefStore);
+ DdmUiPreferences.setThreadRefreshInterval(prefStore.getInt(PREFS_THREAD_REFRESH_INTERVAL));
+ }
+
+ /*
+ * Set default values for all preferences. These are either defined
+ * statically or are based on the values set by the class initializers
+ * in other classes.
+ *
+ * The other threads (e.g. VMWatcherThread) haven't been created yet,
+ * so we want to use static values rather than reading fields from
+ * class.getInstance().
+ */
+ private static void setDefaults(String homeDir) {
+ PreferenceStore prefStore = mStore.getPreferenceStore();
+
+ prefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE);
+
+ prefStore.setDefault(PREFS_SELECTED_DEBUG_PORT,
+ DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT);
+
+ prefStore.setDefault(PREFS_USE_ADBHOST, DdmPreferences.DEFAULT_USE_ADBHOST);
+ prefStore.setDefault(PREFS_ADBHOST_VALUE, DdmPreferences.DEFAULT_ADBHOST_VALUE);
+
+ prefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true);
+ prefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false);
+ prefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL,
+ DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL);
+
+ prefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$
+ prefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$
+
+ prefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$
+
+ prefStore.setDefault(PREFS_TIMEOUT, DdmPreferences.DEFAULT_TIMEOUT);
+ prefStore.setDefault(PREFS_PROFILER_BUFFER_SIZE_MB,
+ DdmPreferences.DEFAULT_PROFILER_BUFFER_SIZE_MB);
+
+ // choose a default font for the text output
+ FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$
+ prefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$
+
+ // layout information.
+ prefStore.setDefault(SHELL_X, 100);
+ prefStore.setDefault(SHELL_Y, 100);
+ prefStore.setDefault(SHELL_WIDTH, 800);
+ prefStore.setDefault(SHELL_HEIGHT, 600);
+
+ prefStore.setDefault(EXPLORER_SHELL_X, 50);
+ prefStore.setDefault(EXPLORER_SHELL_Y, 50);
+
+ prefStore.setDefault(SHOW_NATIVE_HEAP, false);
+ }
+
+
+ /*
+ * Create a "listener" to take action when preferences change. These are
+ * required for ongoing activities that don't check prefs on each use.
+ *
+ * This is only invoked when something explicitly changes the value of
+ * a preference (e.g. not when the prefs file is loaded).
+ */
+ private static class ChangeListener implements IPropertyChangeListener {
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ String changed = event.getProperty();
+ PreferenceStore prefStore = mStore.getPreferenceStore();
+
+ if (changed.equals(PREFS_DEBUG_PORT_BASE)) {
+ DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE));
+ } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) {
+ DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+ } else if (changed.equals(PREFS_LOG_LEVEL)) {
+ DdmPreferences.setLogLevel((String)event.getNewValue());
+ } else if (changed.equals("textSaveDir")) {
+ prefStore.setValue("lastTextSaveDir",
+ (String) event.getNewValue());
+ } else if (changed.equals("imageSaveDir")) {
+ prefStore.setValue("lastImageSaveDir",
+ (String) event.getNewValue());
+ } else if (changed.equals(PREFS_TIMEOUT)) {
+ DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT));
+ } else if (changed.equals(PREFS_PROFILER_BUFFER_SIZE_MB)) {
+ DdmPreferences.setProfilerBufferSizeMb(
+ prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB));
+ } else if (changed.equals(PREFS_USE_ADBHOST)) {
+ DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST));
+ } else if (changed.equals(PREFS_ADBHOST_VALUE)) {
+ DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE));
+ } else {
+ Log.v("ddms", "Preference change: " + event.getProperty()
+ + ": '" + event.getOldValue()
+ + "' --> '" + event.getNewValue() + "'");
+ }
+ }
+ }
+
+
+ /**
+ * Create and display the dialog.
+ */
+ public static void run(Shell shell) {
+ PreferenceStore prefStore = mStore.getPreferenceStore();
+ assert prefStore != null;
+
+ PreferenceManager prefMgr = new PreferenceManager();
+
+ PreferenceNode node, subNode;
+
+ // this didn't work -- got NPE, possibly from class lookup:
+ //PreferenceNode app = new PreferenceNode("app", "Application", null,
+ // AppPrefs.class.getName());
+
+ node = new PreferenceNode("debugger", new DebuggerPrefs());
+ prefMgr.addToRoot(node);
+
+ subNode = new PreferenceNode("panel", new PanelPrefs());
+ //prefMgr.addTo(node.getId(), subNode);
+ prefMgr.addToRoot(subNode);
+
+ node = new PreferenceNode("LogCat", new LogCatPrefs());
+ prefMgr.addToRoot(node);
+
+ node = new PreferenceNode("misc", new MiscPrefs());
+ prefMgr.addToRoot(node);
+
+ node = new PreferenceNode("stats", new UsageStatsPrefs());
+ prefMgr.addToRoot(node);
+
+ PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr);
+ dlg.setPreferenceStore(prefStore);
+
+ // run it
+ try {
+ dlg.open();
+ } catch (Throwable t) {
+ Log.e("ddms", t);
+ }
+
+ // save prefs
+ try {
+ prefStore.save();
+ }
+ catch (IOException ioe) {
+ }
+
+ // discard the stuff we created
+ //prefMgr.dispose();
+ //dlg.dispose();
+ }
+
+ /**
+ * "Debugger" prefs page.
+ */
+ private static class DebuggerPrefs extends FieldEditorPreferencePage {
+
+ private BooleanFieldEditor mUseAdbHost;
+ private StringFieldEditor mAdbHostValue;
+
+ /**
+ * Basic constructor.
+ */
+ public DebuggerPrefs() {
+ super(GRID); // use "grid" layout so edit boxes line up
+ setTitle("Debugger");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ IntegerFieldEditor ife;
+
+ ife = new PortFieldEditor(PREFS_DEBUG_PORT_BASE,
+ "Starting value for local port:", getFieldEditorParent());
+ addField(ife);
+
+ ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT,
+ "Port of Selected VM:", getFieldEditorParent());
+ addField(ife);
+
+ mUseAdbHost = new BooleanFieldEditor(PREFS_USE_ADBHOST,
+ "Use ADBHOST", getFieldEditorParent());
+ addField(mUseAdbHost);
+
+ mAdbHostValue = new StringFieldEditor(PREFS_ADBHOST_VALUE,
+ "ADBHOST value:", getFieldEditorParent());
+ mAdbHostValue.setEnabled(getPreferenceStore()
+ .getBoolean(PREFS_USE_ADBHOST), getFieldEditorParent());
+ addField(mAdbHostValue);
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ // TODO Auto-generated method stub
+ if (event.getSource().equals(mUseAdbHost)) {
+ mAdbHostValue.setEnabled(mUseAdbHost.getBooleanValue(), getFieldEditorParent());
+ }
+ }
+ }
+
+ /**
+ * "Panel" prefs page.
+ */
+ private static class PanelPrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public PanelPrefs() {
+ super(FLAT); // use "flat" layout
+ setTitle("Info Panels");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ BooleanFieldEditor bfe;
+ IntegerFieldEditor ife;
+
+ bfe = new BooleanFieldEditor(PREFS_DEFAULT_THREAD_UPDATE,
+ "Thread updates enabled by default", getFieldEditorParent());
+ addField(bfe);
+
+ bfe = new BooleanFieldEditor(PREFS_DEFAULT_HEAP_UPDATE,
+ "Heap updates enabled by default", getFieldEditorParent());
+ addField(bfe);
+
+ ife = new IntegerFieldEditor(PREFS_THREAD_REFRESH_INTERVAL,
+ "Thread status interval (seconds):", getFieldEditorParent());
+ ife.setValidRange(1, 60);
+ addField(ife);
+ }
+ }
+
+ /**
+ * "logcat" prefs page.
+ */
+ private static class LogCatPrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public LogCatPrefs() {
+ super(FLAT); // use "flat" layout
+ setTitle("Logcat");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ if (UIThread.useOldLogCatView()) {
+ RadioGroupFieldEditor rgfe;
+
+ rgfe = new RadioGroupFieldEditor(PrefsDialog.LOGCAT_COLUMN_MODE,
+ "Message Column Resizing Mode", 1, new String[][] {
+ { "Manual", PrefsDialog.LOGCAT_COLUMN_MODE_MANUAL },
+ { "Automatic", PrefsDialog.LOGCAT_COLUMN_MODE_AUTO },
+ },
+ getFieldEditorParent(), true);
+ addField(rgfe);
+
+ FontFieldEditor ffe = new FontFieldEditor(PrefsDialog.LOGCAT_FONT,
+ "Text output font:",
+ getFieldEditorParent());
+ addField(ffe);
+ } else {
+ FontFieldEditor ffe = new FontFieldEditor(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY,
+ "Text output font:",
+ getFieldEditorParent());
+ addField(ffe);
+
+ IntegerFieldEditor maxMessages = new IntegerFieldEditor(
+ LogCatMessageList.MAX_MESSAGES_PREFKEY,
+ "Maximum number of logcat messages to buffer",
+ getFieldEditorParent());
+ addField(maxMessages);
+
+ BooleanFieldEditor autoScrollLock = new BooleanFieldEditor(
+ LogCatPanel.AUTO_SCROLL_LOCK_PREFKEY,
+ "Automatically enable/disable scroll lock based on the scrollbar position",
+ getFieldEditorParent());
+ addField(autoScrollLock);
+ }
+ }
+ }
+
+ /**
+ * "misc" prefs page.
+ */
+ private static class MiscPrefs extends FieldEditorPreferencePage {
+
+ /**
+ * Basic constructor.
+ */
+ public MiscPrefs() {
+ super(FLAT); // use "flat" layout
+ setTitle("Misc");
+ }
+
+ /**
+ * Create field editors.
+ */
+ @Override
+ protected void createFieldEditors() {
+ DirectoryFieldEditor dfe;
+ FontFieldEditor ffe;
+
+ IntegerFieldEditor ife = new IntegerFieldEditor(PREFS_TIMEOUT,
+ "ADB connection time out (ms):", getFieldEditorParent());
+ addField(ife);
+
+ ife = new IntegerFieldEditor(PREFS_PROFILER_BUFFER_SIZE_MB,
+ "Profiler buffer size (MB):", getFieldEditorParent());
+ addField(ife);
+
+ dfe = new DirectoryFieldEditor("textSaveDir",
+ "Default text save dir:", getFieldEditorParent());
+ addField(dfe);
+
+ dfe = new DirectoryFieldEditor("imageSaveDir",
+ "Default image save dir:", getFieldEditorParent());
+ addField(dfe);
+
+ ffe = new FontFieldEditor("textOutputFont", "Text output font:",
+ getFieldEditorParent());
+ addField(ffe);
+
+ RadioGroupFieldEditor rgfe;
+
+ rgfe = new RadioGroupFieldEditor(PREFS_LOG_LEVEL,
+ "Logging Level", 1, new String[][] {
+ { "Verbose", LogLevel.VERBOSE.getStringValue() },
+ { "Debug", LogLevel.DEBUG.getStringValue() },
+ { "Info", LogLevel.INFO.getStringValue() },
+ { "Warning", LogLevel.WARN.getStringValue() },
+ { "Error", LogLevel.ERROR.getStringValue() },
+ { "Assert", LogLevel.ASSERT.getStringValue() },
+ },
+ getFieldEditorParent(), true);
+ addField(rgfe);
+ }
+ }
+
+ /**
+ * "Device" prefs page.
+ */
+ private static class UsageStatsPrefs extends PreferencePage {
+
+ private BooleanFieldEditor mOptInCheckbox;
+ private Composite mTop;
+
+ /**
+ * Basic constructor.
+ */
+ public UsageStatsPrefs() {
+ setTitle("Usage Stats");
+ }
+
+ @Override
+ protected Control createContents(Composite parent) {
+ mTop = new Composite(parent, SWT.NONE);
+ mTop.setLayout(new GridLayout(1, false));
+ mTop.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Label text = new Label(mTop, SWT.WRAP);
+ text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ text.setText(SdkStatsPermissionDialog.BODY_TEXT);
+
+ Link privacyPolicyLink = new Link(mTop, SWT.WRAP);
+ privacyPolicyLink.setText(SdkStatsPermissionDialog.PRIVACY_POLICY_LINK_TEXT);
+ privacyPolicyLink.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ SdkStatsPermissionDialog.openUrl(event.text);
+ }
+ });
+
+ mOptInCheckbox = new BooleanFieldEditor(DdmsPreferenceStore.PING_OPT_IN,
+ SdkStatsPermissionDialog.CHECKBOX_TEXT, mTop);
+ mOptInCheckbox.setPage(this);
+ mOptInCheckbox.setPreferenceStore(getPreferenceStore());
+ mOptInCheckbox.load();
+
+ return null;
+ }
+
+ @Override
+ protected Point doComputeSize() {
+ if (mTop != null) {
+ return mTop.computeSize(450, SWT.DEFAULT, true);
+ }
+
+ return super.doComputeSize();
+ }
+
+ @Override
+ protected void performDefaults() {
+ if (mOptInCheckbox != null) {
+ mOptInCheckbox.loadDefault();
+ }
+ super.performDefaults();
+ }
+
+ @Override
+ public void performApply() {
+ if (mOptInCheckbox != null) {
+ mOptInCheckbox.store();
+ }
+ super.performApply();
+ }
+
+ @Override
+ public boolean performOk() {
+ if (mOptInCheckbox != null) {
+ mOptInCheckbox.store();
+ }
+ return super.performOk();
+ }
+ }
+
+}
+
+
diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java
new file mode 100644
index 0000000..9a8ada3
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddms;
+
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Dialog to configure the static debug ports.
+ *
+ */
+public class StaticPortConfigDialog extends Dialog {
+
+ /** Preference name for the 0th column width */
+ private static final String PREFS_DEVICE_COL = "spcd.deviceColumn"; //$NON-NLS-1$
+
+ /** Preference name for the 1st column width */
+ private static final String PREFS_APP_COL = "spcd.AppColumn"; //$NON-NLS-1$
+
+ /** Preference name for the 2nd column width */
+ private static final String PREFS_PORT_COL = "spcd.PortColumn"; //$NON-NLS-1$
+
+ private static final int COL_DEVICE = 0;
+ private static final int COL_APPLICATION = 1;
+ private static final int COL_PORT = 2;
+
+
+ private static final int DLG_WIDTH = 500;
+ private static final int DLG_HEIGHT = 300;
+
+ private Shell mShell;
+ private Shell mParent;
+
+ private Table mPortTable;
+
+ /**
+ * Array containing the list of already used static port to avoid
+ * duplication.
+ */
+ private ArrayList<Integer> mPorts = new ArrayList<Integer>();
+
+ /**
+ * Basic constructor.
+ * @param parent
+ */
+ public StaticPortConfigDialog(Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Open and display the dialog. This method returns only when the
+ * user closes the dialog somehow.
+ *
+ */
+ public void open() {
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return;
+ }
+
+ updateFromStore();
+
+ // Set the dialog size.
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.pack();
+
+ // actually open the dialog
+ mShell.open();
+
+ // event loop until the dialog is closed.
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+ }
+
+ /**
+ * Creates the dialog ui.
+ */
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Static Port Configuration");
+
+ mShell.setLayout(new GridLayout(1, true));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ event.doit = true;
+ }
+ });
+
+ // center part with the list on the left and the buttons
+ // on the right.
+ Composite main = new Composite(mShell, SWT.NONE);
+ main.setLayoutData(new GridData(GridData.FILL_BOTH));
+ main.setLayout(new GridLayout(2, false));
+
+ // left part: list view
+ mPortTable = new Table(main, SWT.SINGLE | SWT.FULL_SELECTION);
+ mPortTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mPortTable.setHeaderVisible(true);
+ mPortTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mPortTable, "Device Serial Number",
+ SWT.LEFT, "emulator-5554", //$NON-NLS-1$
+ PREFS_DEVICE_COL, PrefsDialog.getStore());
+
+ TableHelper.createTableColumn(mPortTable, "Application Package",
+ SWT.LEFT, "com.android.samples.phone", //$NON-NLS-1$
+ PREFS_APP_COL, PrefsDialog.getStore());
+
+ TableHelper.createTableColumn(mPortTable, "Debug Port",
+ SWT.RIGHT, "Debug Port", //$NON-NLS-1$
+ PREFS_PORT_COL, PrefsDialog.getStore());
+
+ // right part: buttons
+ Composite buttons = new Composite(main, SWT.NONE);
+ buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+ buttons.setLayout(new GridLayout(1, true));
+
+ Button newButton = new Button(buttons, SWT.NONE);
+ newButton.setText("New...");
+ newButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ StaticPortEditDialog dlg = new StaticPortEditDialog(mShell,
+ mPorts);
+ if (dlg.open()) {
+ // get the text
+ String device = dlg.getDeviceSN();
+ String app = dlg.getAppName();
+ int port = dlg.getPortNumber();
+
+ // add it to the list
+ addEntry(device, app, port);
+ }
+ }
+ });
+
+ final Button editButton = new Button(buttons, SWT.NONE);
+ editButton.setText("Edit...");
+ editButton.setEnabled(false);
+ editButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = mPortTable.getSelectionIndex();
+ String oldDeviceName = getDeviceName(index);
+ String oldAppName = getAppName(index);
+ String oldPortNumber = getPortNumber(index);
+ StaticPortEditDialog dlg = new StaticPortEditDialog(mShell,
+ mPorts, oldDeviceName, oldAppName, oldPortNumber);
+ if (dlg.open()) {
+ // get the text
+ String deviceName = dlg.getDeviceSN();
+ String app = dlg.getAppName();
+ int port = dlg.getPortNumber();
+
+ // add it to the list
+ replaceEntry(index, deviceName, app, port);
+ }
+ }
+ });
+
+ final Button deleteButton = new Button(buttons, SWT.NONE);
+ deleteButton.setText("Delete");
+ deleteButton.setEnabled(false);
+ deleteButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = mPortTable.getSelectionIndex();
+ removeEntry(index);
+ }
+ });
+
+ // bottom part with the ok/cancel
+ Composite bottomComp = new Composite(mShell, SWT.NONE);
+ bottomComp.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_CENTER));
+ bottomComp.setLayout(new GridLayout(2, true));
+
+ Button okButton = new Button(bottomComp, SWT.NONE);
+ okButton.setText("OK");
+ okButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateStore();
+ mShell.close();
+ }
+ });
+
+ Button cancelButton = new Button(bottomComp, SWT.NONE);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ mPortTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the selection index
+ int index = mPortTable.getSelectionIndex();
+
+ boolean enabled = index != -1;
+ editButton.setEnabled(enabled);
+ deleteButton.setEnabled(enabled);
+ }
+ });
+
+ mShell.pack();
+
+ }
+
+ /**
+ * Add a new entry in the list.
+ * @param deviceName the serial number of the device
+ * @param appName java package for the application
+ * @param portNumber port number
+ */
+ private void addEntry(String deviceName, String appName, int portNumber) {
+ // create a new item for the table
+ TableItem item = new TableItem(mPortTable, SWT.NONE);
+
+ item.setText(COL_DEVICE, deviceName);
+ item.setText(COL_APPLICATION, appName);
+ item.setText(COL_PORT, Integer.toString(portNumber));
+
+ // add the port to the list of port number used.
+ mPorts.add(portNumber);
+ }
+
+ /**
+ * Remove an entry from the list.
+ * @param index The index of the entry to be removed
+ */
+ private void removeEntry(int index) {
+ // remove from the ui
+ mPortTable.remove(index);
+
+ // and from the port list.
+ mPorts.remove(index);
+ }
+
+ /**
+ * Replace an entry in the list with new values.
+ * @param index The index of the item to be replaced
+ * @param deviceName the serial number of the device
+ * @param appName The new java package for the application
+ * @param portNumber The new port number.
+ */
+ private void replaceEntry(int index, String deviceName, String appName, int portNumber) {
+ // get the table item by index
+ TableItem item = mPortTable.getItem(index);
+
+ // set its new value
+ item.setText(COL_DEVICE, deviceName);
+ item.setText(COL_APPLICATION, appName);
+ item.setText(COL_PORT, Integer.toString(portNumber));
+
+ // and replace the port number in the port list.
+ mPorts.set(index, portNumber);
+ }
+
+
+ /**
+ * Returns the device name for a specific index
+ * @param index The index
+ * @return the java package name of the application
+ */
+ private String getDeviceName(int index) {
+ TableItem item = mPortTable.getItem(index);
+ return item.getText(COL_DEVICE);
+ }
+
+ /**
+ * Returns the application name for a specific index
+ * @param index The index
+ * @return the java package name of the application
+ */
+ private String getAppName(int index) {
+ TableItem item = mPortTable.getItem(index);
+ return item.getText(COL_APPLICATION);
+ }
+
+ /**
+ * Returns the port number for a specific index
+ * @param index The index
+ * @return the port number
+ */
+ private String getPortNumber(int index) {
+ TableItem item = mPortTable.getItem(index);
+ return item.getText(COL_PORT);
+ }
+
+ /**
+ * Updates the ui from the value in the preference store.
+ */
+ private void updateFromStore() {
+ // get the map from the debug port manager
+ DebugPortProvider provider = DebugPortProvider.getInstance();
+ Map<String, Map<String, Integer>> map = provider.getPortList();
+
+ // we're going to loop on the keys and fill the table.
+ Set<String> deviceKeys = map.keySet();
+
+ for (String deviceKey : deviceKeys) {
+ Map<String, Integer> deviceMap = map.get(deviceKey);
+ if (deviceMap != null) {
+ Set<String> appKeys = deviceMap.keySet();
+
+ for (String appKey : appKeys) {
+ Integer port = deviceMap.get(appKey);
+ if (port != null) {
+ addEntry(deviceKey, appKey, port);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the store from the content of the ui.
+ */
+ private void updateStore() {
+ // create a new Map object and fill it.
+ HashMap<String, Map<String, Integer>> map = new HashMap<String, Map<String, Integer>>();
+
+ int count = mPortTable.getItemCount();
+
+ for (int i = 0 ; i < count ; i++) {
+ TableItem item = mPortTable.getItem(i);
+ String deviceName = item.getText(COL_DEVICE);
+
+ Map<String, Integer> deviceMap = map.get(deviceName);
+ if (deviceMap == null) {
+ deviceMap = new HashMap<String, Integer>();
+ map.put(deviceName, deviceMap);
+ }
+
+ deviceMap.put(item.getText(COL_APPLICATION), Integer.valueOf(item.getText(COL_PORT)));
+ }
+
+ // set it in the store through the debug port manager.
+ DebugPortProvider provider = DebugPortProvider.getInstance();
+ provider.setPortList(map);
+ }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java
new file mode 100644
index 0000000..c9cb044
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddms;
+
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+
+/**
+ * Small dialog box to edit a static port number.
+ */
+public class StaticPortEditDialog extends Dialog {
+
+ private static final int DLG_WIDTH = 400;
+ private static final int DLG_HEIGHT = 200;
+
+ private Shell mParent;
+
+ private Shell mShell;
+
+ private boolean mOk = false;
+
+ private String mAppName;
+
+ private String mPortNumber;
+
+ private Button mOkButton;
+
+ private Label mWarning;
+
+ /** List of ports already in use */
+ private ArrayList<Integer> mPorts;
+
+ /** This is the port being edited. */
+ private int mEditPort = -1;
+ private String mDeviceSn;
+
+ /**
+ * Creates a dialog with empty fields.
+ * @param parent The parent Shell
+ * @param ports The list of already used port numbers.
+ */
+ public StaticPortEditDialog(Shell parent, ArrayList<Integer> ports) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ mPorts = ports;
+ mDeviceSn = IDevice.FIRST_EMULATOR_SN;
+ }
+
+ /**
+ * Creates a dialog with predefined values.
+ * @param shell The parent shell
+ * @param ports The list of already used port numbers.
+ * @param oldDeviceSN the device serial number to display
+ * @param oldAppName The application name to display
+ * @param oldPortNumber The port number to display
+ */
+ public StaticPortEditDialog(Shell shell, ArrayList<Integer> ports,
+ String oldDeviceSN, String oldAppName, String oldPortNumber) {
+ this(shell, ports);
+
+ mDeviceSn = oldDeviceSN;
+ mAppName = oldAppName;
+ mPortNumber = oldPortNumber;
+ mEditPort = Integer.valueOf(mPortNumber);
+ }
+
+ /**
+ * Opens the dialog. The method will return when the user closes the dialog
+ * somehow.
+ *
+ * @return true if ok was pressed, false if cancelled.
+ */
+ public boolean open() {
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.open();
+
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ return mOk;
+ }
+
+ public String getDeviceSN() {
+ return mDeviceSn;
+ }
+
+ public String getAppName() {
+ return mAppName;
+ }
+
+ public int getPortNumber() {
+ return Integer.valueOf(mPortNumber);
+ }
+
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Static Port");
+
+ mShell.setLayout(new GridLayout(1, false));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ }
+ });
+
+ // center part with the edit field
+ Composite main = new Composite(mShell, SWT.NONE);
+ main.setLayoutData(new GridData(GridData.FILL_BOTH));
+ main.setLayout(new GridLayout(2, false));
+
+ Label l0 = new Label(main, SWT.NONE);
+ l0.setText("Device Name:");
+
+ final Text deviceSNText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ deviceSNText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ if (mDeviceSn != null) {
+ deviceSNText.setText(mDeviceSn);
+ }
+ deviceSNText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mDeviceSn = deviceSNText.getText().trim();
+ validate();
+ }
+ });
+
+ Label l = new Label(main, SWT.NONE);
+ l.setText("Application Name:");
+
+ final Text appNameText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mAppName != null) {
+ appNameText.setText(mAppName);
+ }
+ appNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ appNameText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mAppName = appNameText.getText().trim();
+ validate();
+ }
+ });
+
+ Label l2 = new Label(main, SWT.NONE);
+ l2.setText("Debug Port:");
+
+ final Text debugPortText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mPortNumber != null) {
+ debugPortText.setText(mPortNumber);
+ }
+ debugPortText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ debugPortText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mPortNumber = debugPortText.getText().trim();
+ validate();
+ }
+ });
+
+ // warning label
+ Composite warningComp = new Composite(mShell, SWT.NONE);
+ warningComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ warningComp.setLayout(new GridLayout(1, true));
+
+ mWarning = new Label(warningComp, SWT.NONE);
+ mWarning.setText("");
+ mWarning.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // bottom part with the ok/cancel
+ Composite bottomComp = new Composite(mShell, SWT.NONE);
+ bottomComp
+ .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ bottomComp.setLayout(new GridLayout(2, true));
+
+ mOkButton = new Button(bottomComp, SWT.NONE);
+ mOkButton.setText("OK");
+ mOkButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mOk = true;
+ mShell.close();
+ }
+ });
+ mOkButton.setEnabled(false);
+ mShell.setDefaultButton(mOkButton);
+
+ Button cancelButton = new Button(bottomComp, SWT.NONE);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Validates the content of the 2 text fields and enable/disable "ok", while
+ * setting up the warning/error message.
+ */
+ private void validate() {
+ // first we reset the warning dialog. This allows us to latter
+ // display warnings.
+ mWarning.setText(""); //$NON-NLS-1$
+
+ // check the device name field is not empty
+ if (mDeviceSn == null || mDeviceSn.length() == 0) {
+ mWarning.setText("Device name missing.");
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // check the application name field is not empty
+ if (mAppName == null || mAppName.length() == 0) {
+ mWarning.setText("Application name missing.");
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ String packageError = "Application name must be a valid Java package name.";
+
+ // validate the package name as well. It must be a fully qualified
+ // java package.
+ String[] packageSegments = mAppName.split("\\."); //$NON-NLS-1$
+ for (String p : packageSegments) {
+ if (p.matches("^[a-zA-Z][a-zA-Z0-9]*") == false) { //$NON-NLS-1$
+ mWarning.setText(packageError);
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // lets also display a warning if the package contains upper case
+ // letters.
+ if (p.matches("^[a-z][a-z0-9]*") == false) { //$NON-NLS-1$
+ mWarning.setText("Lower case is recommended for Java packages.");
+ }
+ }
+
+ // the split will not detect the last char being a '.'
+ // so we test it manually
+ if (mAppName.charAt(mAppName.length()-1) == '.') {
+ mWarning.setText(packageError);
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // now we test the package name field is not empty.
+ if (mPortNumber == null || mPortNumber.length() == 0) {
+ mWarning.setText("Port Number missing.");
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // then we check it only contains digits.
+ if (mPortNumber.matches("[0-9]*") == false) { //$NON-NLS-1$
+ mWarning.setText("Port Number invalid.");
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // get the int from the port number to validate
+ long port = Long.valueOf(mPortNumber);
+ if (port >= 32767) {
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ // check if its in the list of already used ports
+ if (port != mEditPort) {
+ for (Integer i : mPorts) {
+ if (port == i.intValue()) {
+ mWarning.setText("Port already in use.");
+ mOkButton.setEnabled(false);
+ return;
+ }
+ }
+ }
+
+ // at this point there's not error, so we enable the ok button.
+ mOkButton.setEnabled(true);
+ }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/UIThread.java b/ddms/app/src/main/java/com/android/ddms/UIThread.java
new file mode 100644
index 0000000..8aaa806
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/UIThread.java
@@ -0,0 +1,1803 @@
+/*
+ * 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.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+import com.android.ddmlib.ClientData.MethodProfilingStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.ILogOutput;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmuilib.AllocationPanel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.DevicePanel;
+import com.android.ddmuilib.DevicePanel.IUiSelectionListener;
+import com.android.ddmuilib.EmulatorControlPanel;
+import com.android.ddmuilib.HeapPanel;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.InfoPanel;
+import com.android.ddmuilib.NativeHeapPanel;
+import com.android.ddmuilib.ScreenShotDialog;
+import com.android.ddmuilib.SysinfoPanel;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.ThreadPanel;
+import com.android.ddmuilib.actions.ToolItemAction;
+import com.android.ddmuilib.explorer.DeviceExplorer;
+import com.android.ddmuilib.handler.BaseFileHandler;
+import com.android.ddmuilib.handler.MethodProfilingHandler;
+import com.android.ddmuilib.log.event.EventLogPanel;
+import com.android.ddmuilib.logcat.LogCatPanel;
+import com.android.ddmuilib.logcat.LogColors;
+import com.android.ddmuilib.logcat.LogFilter;
+import com.android.ddmuilib.logcat.LogPanel;
+import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager;
+import com.android.ddmuilib.net.NetworkPanel;
+import com.android.menubar.IMenuBarCallback;
+import com.android.menubar.IMenuBarEnhancer;
+import com.android.menubar.IMenuBarEnhancer.MenuBarMode;
+import com.android.menubar.MenuBarEnhancer;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.MenuAdapter;
+import org.eclipse.swt.events.MenuEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.events.ShellListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an
+ * SWT application. So this class mainly builds the ui, and manages communication between the panels
+ * when {@link IDevice} / {@link Client} selection changes.
+ */
+public class UIThread implements IUiSelectionListener, IClientChangeListener {
+ public static final String APP_NAME = "DDMS";
+
+ /*
+ * UI tab panel definitions. The constants here must match up with the array
+ * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing
+ * the client list.
+ */
+ public static final int PANEL_CLIENT_LIST = -1;
+
+ public static final int PANEL_INFO = 0;
+
+ public static final int PANEL_THREAD = 1;
+
+ public static final int PANEL_HEAP = 2;
+
+ private static final int PANEL_NATIVE_HEAP = 3;
+
+ private static final int PANEL_ALLOCATIONS = 4;
+
+ private static final int PANEL_SYSINFO = 5;
+
+ private static final int PANEL_NETWORK = 6;
+
+ private static final int PANEL_COUNT = 7;
+
+ /** Content is setup in the constructor */
+ private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT];
+
+ private static final String[] mPanelNames = new String[] {
+ "Info", "Threads", "VM Heap", "Native Heap",
+ "Allocation Tracker", "Sysinfo", "Network"
+ };
+
+ private static final String[] mPanelTips = new String[] {
+ "Client information", "Thread status", "VM heap status",
+ "Native heap status", "Allocation Tracker", "Sysinfo graphs",
+ "Network usage"
+ };
+
+ private static final String PREFERENCE_LOGSASH =
+ "logSashLocation"; //$NON-NLS-1$
+ private static final String PREFERENCE_SASH =
+ "sashLocation"; //$NON-NLS-1$
+
+ private static final String PREFS_COL_TIME =
+ "logcat.time"; //$NON-NLS-1$
+ private static final String PREFS_COL_LEVEL =
+ "logcat.level"; //$NON-NLS-1$
+ private static final String PREFS_COL_PID =
+ "logcat.pid"; //$NON-NLS-1$
+ private static final String PREFS_COL_TAG =
+ "logcat.tag"; //$NON-NLS-1$
+ private static final String PREFS_COL_MESSAGE =
+ "logcat.message"; //$NON-NLS-1$
+
+ private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$
+
+ // singleton instance
+ private static UIThread mInstance = new UIThread();
+
+ // our display
+ private Display mDisplay;
+
+ // the table we show in the left-hand pane
+ private DevicePanel mDevicePanel;
+
+ private IDevice mCurrentDevice = null;
+ private Client mCurrentClient = null;
+
+ // status line at the bottom of the app window
+ private Label mStatusLine;
+
+ // some toolbar items we need to update
+ private ToolItem mTBShowThreadUpdates;
+ private ToolItem mTBShowHeapUpdates;
+ private ToolItem mTBHalt;
+ private ToolItem mTBCauseGc;
+ private ToolItem mTBDumpHprof;
+ private ToolItem mTBProfiling;
+
+ private final class FilterStorage implements ILogFilterStorageManager {
+
+ @Override
+ public LogFilter[] getFilterFromStore() {
+ String filterPrefs = PrefsDialog.getStore().getString(
+ PREFS_FILTERS);
+
+ // split in a string per filter
+ String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$
+
+ ArrayList<LogFilter> list =
+ new ArrayList<LogFilter>(filters.length);
+
+ for (String f : filters) {
+ if (f.length() > 0) {
+ LogFilter logFilter = new LogFilter();
+ if (logFilter.loadFromString(f)) {
+ list.add(logFilter);
+ }
+ }
+ }
+
+ return list.toArray(new LogFilter[list.size()]);
+ }
+
+ @Override
+ public void saveFilters(LogFilter[] filters) {
+ StringBuilder sb = new StringBuilder();
+ for (LogFilter f : filters) {
+ String filterString = f.toString();
+ sb.append(filterString);
+ sb.append('|');
+ }
+
+ PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString());
+ }
+
+ @Override
+ public boolean requiresDefaultFilter() {
+ return true;
+ }
+ }
+
+
+ /**
+ * Flag to indicate whether to use the old or the new logcat view. This is a
+ * temporary workaround that will be removed once the new view is complete.
+ */
+ private static final String USE_OLD_LOGCAT_VIEW =
+ System.getenv("ANDROID_USE_OLD_LOGCAT_VIEW");
+ public static boolean useOldLogCatView() {
+ return USE_OLD_LOGCAT_VIEW != null;
+ }
+
+ private LogPanel mLogPanel; /* only valid when useOldLogCatView() == true */
+ private LogCatPanel mLogCatPanel; /* only valid when useOldLogCatView() == false */
+
+ private ToolItemAction mCreateFilterAction;
+ private ToolItemAction mDeleteFilterAction;
+ private ToolItemAction mEditFilterAction;
+ private ToolItemAction mExportAction;
+ private ToolItemAction mClearAction;
+
+ private ToolItemAction[] mLogLevelActions;
+ private String[] mLogLevelIcons = {
+ "v.png", //$NON-NLS-1S
+ "d.png", //$NON-NLS-1S
+ "i.png", //$NON-NLS-1S
+ "w.png", //$NON-NLS-1S
+ "e.png", //$NON-NLS-1S
+ };
+
+ protected Clipboard mClipboard;
+
+ private MenuItem mCopyMenuItem;
+
+ private MenuItem mSelectAllMenuItem;
+
+ private TableFocusListener mTableListener;
+
+ private DeviceExplorer mExplorer = null;
+ private Shell mExplorerShell = null;
+
+ private EmulatorControlPanel mEmulatorPanel;
+
+ private EventLogPanel mEventLogPanel;
+
+ private Image mTracingStartImage;
+
+ private Image mTracingStopImage;
+
+ private ImageLoader mDdmUiLibLoader;
+
+ private class TableFocusListener implements ITableFocusListener {
+
+ private IFocusedTableActivator mCurrentActivator;
+
+ @Override
+ public void focusGained(IFocusedTableActivator activator) {
+ mCurrentActivator = activator;
+ if (mCopyMenuItem.isDisposed() == false) {
+ mCopyMenuItem.setEnabled(true);
+ mSelectAllMenuItem.setEnabled(true);
+ }
+ }
+
+ @Override
+ public void focusLost(IFocusedTableActivator activator) {
+ // if we move from one table to another, it's unclear
+ // if the old table lose its focus before the new
+ // one gets the focus, so we need to check.
+ if (activator == mCurrentActivator) {
+ activator = null;
+ if (mCopyMenuItem.isDisposed() == false) {
+ mCopyMenuItem.setEnabled(false);
+ mSelectAllMenuItem.setEnabled(false);
+ }
+ }
+ }
+
+ public void copy(Clipboard clipboard) {
+ if (mCurrentActivator != null) {
+ mCurrentActivator.copy(clipboard);
+ }
+ }
+
+ public void selectAll() {
+ if (mCurrentActivator != null) {
+ mCurrentActivator.selectAll();
+ }
+ }
+ }
+
+ /**
+ * Handler for HPROF dumps.
+ * This will always prompt the user to save the HPROF file.
+ */
+ private class HProfHandler extends BaseFileHandler implements IHprofDumpHandler {
+
+ public HProfHandler(Shell parentShell) {
+ super(parentShell);
+ }
+
+ @Override
+ public void onEndFailure(final Client client, final String message) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ displayErrorFromUiThread(
+ "Unable to create HPROF file for application '%1$s'\n\n%2$s" +
+ "Check logcat for more information.",
+ client.getClientData().getClientDescription(),
+ message != null ? message + "\n\n" : "");
+ } finally {
+ // this will make sure the dump hprof button is re-enabled for the
+ // current selection. as the client is finished dumping an hprof file
+ enableButtons();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSuccess(final String remoteFilePath, final Client client) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ final IDevice device = client.getDevice();
+ try {
+ // get the sync service to pull the HPROF file
+ final SyncService sync = client.getDevice().getSyncService();
+ if (sync != null) {
+ promptAndPull(sync,
+ client.getClientData().getClientDescription() + ".hprof",
+ remoteFilePath, "Save HPROF file");
+ } else {
+ displayErrorFromUiThread(
+ "Unable to download HPROF file from device '%1$s'.",
+ device.getSerialNumber());
+ }
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ displayErrorFromUiThread(
+ "Unable to download HPROF file from device '%1$s'.\n\n%2$s",
+ device.getSerialNumber(), e.getMessage());
+ }
+ } catch (Exception e) {
+ displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.",
+ device.getSerialNumber());
+
+ } finally {
+ // this will make sure the dump hprof button is re-enabled for the
+ // current selection. as the client is finished dumping an hprof file
+ enableButtons();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSuccess(final byte[] data, final Client client) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ promptAndSave(client.getClientData().getClientDescription() + ".hprof", data,
+ "Save HPROF file");
+ }
+ });
+ }
+
+ @Override
+ protected String getDialogTitle() {
+ return "HPROF Error";
+ }
+ }
+
+
+ /**
+ * Generic constructor.
+ */
+ private UIThread() {
+ mPanels[PANEL_INFO] = new InfoPanel();
+ mPanels[PANEL_THREAD] = new ThreadPanel();
+ mPanels[PANEL_HEAP] = new HeapPanel();
+ if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) {
+ if (System.getenv("ANDROID_DDMS_OLD_HEAP_PANEL") != null) {
+ mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel();
+ } else {
+ mPanels[PANEL_NATIVE_HEAP] =
+ new com.android.ddmuilib.heap.NativeHeapPanel(getStore());
+ }
+ } else {
+ mPanels[PANEL_NATIVE_HEAP] = null;
+ }
+ mPanels[PANEL_ALLOCATIONS] = new AllocationPanel();
+ mPanels[PANEL_SYSINFO] = new SysinfoPanel();
+ mPanels[PANEL_NETWORK] = new NetworkPanel();
+ }
+
+ /**
+ * Get singleton instance of the UI thread.
+ */
+ public static UIThread getInstance() {
+ return mInstance;
+ }
+
+ /**
+ * Return the Display. Don't try this unless you're in the UI thread.
+ */
+ public Display getDisplay() {
+ return mDisplay;
+ }
+
+ public void asyncExec(Runnable r) {
+ if (mDisplay != null && mDisplay.isDisposed() == false) {
+ mDisplay.asyncExec(r);
+ }
+ }
+
+ /** returns the IPreferenceStore */
+ public IPreferenceStore getStore() {
+ return PrefsDialog.getStore();
+ }
+
+ /**
+ * Create SWT objects and drive the user interface event loop.
+ * @param ddmsParentLocation location of the folder that contains ddms.
+ */
+ public void runUI(String ddmsParentLocation) {
+ Display.setAppName(APP_NAME);
+ mDisplay = Display.getDefault();
+ final Shell shell = new Shell(mDisplay, SWT.SHELL_TRIM);
+
+ // create the image loaders for DDMS and DDMUILIB
+ mDdmUiLibLoader = ImageLoader.getDdmUiLibLoader();
+
+ shell.setImage(ImageLoader.getLoader(this.getClass()).loadImage(mDisplay,
+ "ddms-128.png", //$NON-NLS-1$
+ 100, 50, null));
+
+ Log.setLogOutput(new ILogOutput() {
+ @Override
+ public void printAndPromptLog(final LogLevel logLevel, final String tag,
+ final String message) {
+ Log.printLog(logLevel, tag, message);
+ // dialog box only run in UI thread..
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ Shell activeShell = mDisplay.getActiveShell();
+ if (logLevel == LogLevel.ERROR) {
+ MessageDialog.openError(activeShell, tag, message);
+ } else {
+ MessageDialog.openWarning(activeShell, tag, message);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void printLog(LogLevel logLevel, String tag, String message) {
+ Log.printLog(logLevel, tag, message);
+ }
+ });
+
+ // set the handler for hprof dump
+ ClientData.setHprofDumpHandler(new HProfHandler(shell));
+ ClientData.setMethodProfilingHandler(new MethodProfilingHandler(shell));
+
+ // [try to] ensure ADB is running
+ // in the new SDK, adb is in the platform-tools, but when run from the command line
+ // in the Android source tree, then adb is next to ddms.
+ String adbLocation;
+ if (ddmsParentLocation != null && ddmsParentLocation.length() != 0) {
+ // check if there's a platform-tools folder
+ File platformTools = new File(new File(ddmsParentLocation).getParent(),
+ "platform-tools"); //$NON-NLS-1$
+ if (platformTools.isDirectory()) {
+ adbLocation = platformTools.getAbsolutePath() + File.separator + "adb"; //$NON-NLS-1$
+ } else {
+ adbLocation = ddmsParentLocation + File.separator + "adb"; //$NON-NLS-1$
+ }
+ } else {
+ adbLocation = "adb"; //$NON-NLS-1$
+ }
+
+ AndroidDebugBridge.init(true /* debugger support */);
+ AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */);
+
+ // we need to listen to client change to be notified of client status (profiling) change
+ AndroidDebugBridge.addClientChangeListener(this);
+
+ shell.setText("Dalvik Debug Monitor");
+ setConfirmClose(shell);
+ createMenus(shell);
+ createWidgets(shell);
+
+ shell.pack();
+ setSizeAndPosition(shell);
+ shell.open();
+
+ Log.d("ddms", "UI is up");
+
+ while (!shell.isDisposed()) {
+ if (!mDisplay.readAndDispatch())
+ mDisplay.sleep();
+ }
+ if (useOldLogCatView()) {
+ mLogPanel.stopLogCat(true);
+ }
+
+ mDevicePanel.dispose();
+ for (TablePanel panel : mPanels) {
+ if (panel != null) {
+ panel.dispose();
+ }
+ }
+
+ ImageLoader.dispose();
+
+ mDisplay.dispose();
+ Log.d("ddms", "UI is down");
+ }
+
+ /**
+ * Set the size and position of the main window from the preference, and
+ * setup listeners for control events (resize/move of the window)
+ */
+ private void setSizeAndPosition(final Shell shell) {
+ shell.setMinimumSize(400, 200);
+
+ // get the x/y and w/h from the prefs
+ PreferenceStore prefs = PrefsDialog.getStore();
+ int x = prefs.getInt(PrefsDialog.SHELL_X);
+ int y = prefs.getInt(PrefsDialog.SHELL_Y);
+ int w = prefs.getInt(PrefsDialog.SHELL_WIDTH);
+ int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT);
+
+ // check that we're not out of the display area
+ Rectangle rect = mDisplay.getClientArea();
+ // first check the width/height
+ if (w > rect.width) {
+ w = rect.width;
+ prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width);
+ }
+ if (h > rect.height) {
+ h = rect.height;
+ prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height);
+ }
+ // then check x. Make sure the left corner is in the screen
+ if (x < rect.x) {
+ x = rect.x;
+ prefs.setValue(PrefsDialog.SHELL_X, rect.x);
+ } else if (x >= rect.x + rect.width) {
+ x = rect.x + rect.width - w;
+ prefs.setValue(PrefsDialog.SHELL_X, rect.x);
+ }
+ // then check y. Make sure the left corner is in the screen
+ if (y < rect.y) {
+ y = rect.y;
+ prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
+ } else if (y >= rect.y + rect.height) {
+ y = rect.y + rect.height - h;
+ prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
+ }
+
+ // now we can set the location/size
+ shell.setBounds(x, y, w, h);
+
+ // add listener for resize/move
+ shell.addControlListener(new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ // get the new x/y
+ Rectangle controlBounds = shell.getBounds();
+ // store in pref file
+ PreferenceStore currentPrefs = PrefsDialog.getStore();
+ currentPrefs.setValue(PrefsDialog.SHELL_X, controlBounds.x);
+ currentPrefs.setValue(PrefsDialog.SHELL_Y, controlBounds.y);
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ // get the new w/h
+ Rectangle controlBounds = shell.getBounds();
+ // store in pref file
+ PreferenceStore currentPrefs = PrefsDialog.getStore();
+ currentPrefs.setValue(PrefsDialog.SHELL_WIDTH, controlBounds.width);
+ currentPrefs.setValue(PrefsDialog.SHELL_HEIGHT, controlBounds.height);
+ }
+ });
+ }
+
+ /**
+ * Set the size and position of the file explorer window from the
+ * preference, and setup listeners for control events (resize/move of
+ * the window)
+ */
+ private void setExplorerSizeAndPosition(final Shell shell) {
+ shell.setMinimumSize(400, 200);
+
+ // get the x/y and w/h from the prefs
+ PreferenceStore prefs = PrefsDialog.getStore();
+ int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X);
+ int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y);
+ int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH);
+ int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT);
+
+ // check that we're not out of the display area
+ Rectangle rect = mDisplay.getClientArea();
+ // first check the width/height
+ if (w > rect.width) {
+ w = rect.width;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width);
+ }
+ if (h > rect.height) {
+ h = rect.height;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height);
+ }
+ // then check x. Make sure the left corner is in the screen
+ if (x < rect.x) {
+ x = rect.x;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
+ } else if (x >= rect.x + rect.width) {
+ x = rect.x + rect.width - w;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
+ }
+ // then check y. Make sure the left corner is in the screen
+ if (y < rect.y) {
+ y = rect.y;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
+ } else if (y >= rect.y + rect.height) {
+ y = rect.y + rect.height - h;
+ prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
+ }
+
+ // now we can set the location/size
+ shell.setBounds(x, y, w, h);
+
+ // add listener for resize/move
+ shell.addControlListener(new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ // get the new x/y
+ Rectangle controlBounds = shell.getBounds();
+ // store in pref file
+ PreferenceStore currentPrefs = PrefsDialog.getStore();
+ currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_X, controlBounds.x);
+ currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, controlBounds.y);
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ // get the new w/h
+ Rectangle controlBounds = shell.getBounds();
+ // store in pref file
+ PreferenceStore currentPrefs = PrefsDialog.getStore();
+ currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, controlBounds.width);
+ currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, controlBounds.height);
+ }
+ });
+ }
+
+ /*
+ * Set the confirm-before-close dialog.
+ */
+ private void setConfirmClose(final Shell shell) {
+ // Note: there was some commented out code to display a confirmation box
+ // when closing. The feature seems unnecessary and the code was not being
+ // used, so it has been removed.
+ }
+
+ /*
+ * Create the menu bar and items.
+ */
+ private void createMenus(final Shell shell) {
+ // create menu bar
+ Menu menuBar = new Menu(shell, SWT.BAR);
+
+ // create top-level items
+ MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE);
+ fileItem.setText("&File");
+ MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE);
+ editItem.setText("&Edit");
+ MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE);
+ actionItem.setText("&Actions");
+ MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE);
+ deviceItem.setText("&Device");
+
+ // create top-level menus
+ Menu fileMenu = new Menu(menuBar);
+ fileItem.setMenu(fileMenu);
+ Menu editMenu = new Menu(menuBar);
+ editItem.setMenu(editMenu);
+ Menu actionMenu = new Menu(menuBar);
+ actionItem.setMenu(actionMenu);
+ Menu deviceMenu = new Menu(menuBar);
+ deviceItem.setMenu(deviceMenu);
+
+ MenuItem item;
+
+ // create File menu items
+ item = new MenuItem(fileMenu, SWT.NONE);
+ item.setText("&Static Port Configuration...");
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell);
+ dlg.open();
+ }
+ });
+
+ IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenu(APP_NAME, fileMenu,
+ new IMenuBarCallback() {
+ @Override
+ public void printError(String format, Object... args) {
+ Log.e("DDMS Menu Bar", String.format(format, args));
+ }
+
+ @Override
+ public void onPreferencesMenuSelected() {
+ PrefsDialog.run(shell);
+ }
+
+ @Override
+ public void onAboutMenuSelected() {
+ AboutDialog dlg = new AboutDialog(shell);
+ dlg.open();
+ }
+ });
+
+ if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
+ new MenuItem(fileMenu, SWT.SEPARATOR);
+
+ item = new MenuItem(fileMenu, SWT.NONE);
+ item.setText("E&xit\tCtrl-Q");
+ item.setAccelerator('Q' | SWT.MOD1);
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ shell.close();
+ }
+ });
+ }
+
+ // create edit menu items
+ mCopyMenuItem = new MenuItem(editMenu, SWT.NONE);
+ mCopyMenuItem.setText("&Copy\tCtrl-C");
+ mCopyMenuItem.setAccelerator('C' | SWT.MOD1);
+ mCopyMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mTableListener.copy(mClipboard);
+ }
+ });
+
+ new MenuItem(editMenu, SWT.SEPARATOR);
+
+ mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE);
+ mSelectAllMenuItem.setText("Select &All\tCtrl-A");
+ mSelectAllMenuItem.setAccelerator('A' | SWT.MOD1);
+ mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mTableListener.selectAll();
+ }
+ });
+
+ // create Action menu items
+ // TODO: this should come with a confirmation dialog
+ final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE);
+ actionHaltItem.setText("&Halt VM");
+ actionHaltItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.killSelectedClient();
+ }
+ });
+
+ final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE);
+ actionCauseGcItem.setText("Cause &GC");
+ actionCauseGcItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.forceGcOnSelectedClient();
+ }
+ });
+
+ final MenuItem actionResetAdb = new MenuItem(actionMenu, SWT.NONE);
+ actionResetAdb.setText("&Reset adb");
+ actionResetAdb.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+ if (bridge != null) {
+ bridge.restart();
+ }
+ }
+ });
+
+ // configure Action items based on current state
+ actionMenu.addMenuListener(new MenuAdapter() {
+ @Override
+ public void menuShown(MenuEvent e) {
+ actionHaltItem.setEnabled(mTBHalt.getEnabled() && mCurrentClient != null);
+ actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled() && mCurrentClient != null);
+ actionResetAdb.setEnabled(true);
+ }
+ });
+
+ // create Device menu items
+ final MenuItem screenShotItem = new MenuItem(deviceMenu, SWT.NONE);
+
+ // The \tCtrl-S "keybinding text" here isn't right for the Mac - but
+ // it's stripped out and replaced by the proper keyboard accelerator
+ // text (e.g. the unicode symbol for the command key + S) anyway
+ // so it's fine to leave it there for the other platforms.
+ screenShotItem.setText("&Screen capture...\tCtrl-S");
+ screenShotItem.setAccelerator('S' | SWT.MOD1);
+ screenShotItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mCurrentDevice != null) {
+ ScreenShotDialog dlg = new ScreenShotDialog(shell);
+ dlg.open(mCurrentDevice);
+ }
+ }
+ });
+
+ new MenuItem(deviceMenu, SWT.SEPARATOR);
+
+ final MenuItem explorerItem = new MenuItem(deviceMenu, SWT.NONE);
+ explorerItem.setText("File Explorer...");
+ explorerItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ createFileExplorer();
+ }
+ });
+
+ new MenuItem(deviceMenu, SWT.SEPARATOR);
+
+ final MenuItem processItem = new MenuItem(deviceMenu, SWT.NONE);
+ processItem.setText("Show &process status...");
+ processItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ final MenuItem deviceStateItem = new MenuItem(deviceMenu, SWT.NONE);
+ deviceStateItem.setText("Dump &device state...");
+ deviceStateItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0",
+ "device-state.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ final MenuItem appStateItem = new MenuItem(deviceMenu, SWT.NONE);
+ appStateItem.setText("Dump &app state...");
+ appStateItem.setEnabled(false);
+ appStateItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ final MenuItem radioStateItem = new MenuItem(deviceMenu, SWT.NONE);
+ radioStateItem.setText("Dump &radio state...");
+ radioStateItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog(
+ "cat /data/logs/radio.4 /data/logs/radio.3"
+ + " /data/logs/radio.2 /data/logs/radio.1"
+ + " /data/logs/radio",
+ "radio-state.txt", shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ final MenuItem logCatItem = new MenuItem(deviceMenu, SWT.NONE);
+ logCatItem.setText("Run &logcat...");
+ logCatItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ DeviceCommandDialog dlg;
+ dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt",
+ shell);
+ dlg.open(mCurrentDevice);
+ }
+ });
+
+ // configure Action items based on current state
+ deviceMenu.addMenuListener(new MenuAdapter() {
+ @Override
+ public void menuShown(MenuEvent e) {
+ boolean deviceEnabled = mCurrentDevice != null;
+ screenShotItem.setEnabled(deviceEnabled);
+ explorerItem.setEnabled(deviceEnabled);
+ processItem.setEnabled(deviceEnabled);
+ deviceStateItem.setEnabled(deviceEnabled);
+ appStateItem.setEnabled(deviceEnabled);
+ radioStateItem.setEnabled(deviceEnabled);
+ logCatItem.setEnabled(deviceEnabled);
+ }
+ });
+
+ // tell the shell to use this menu
+ shell.setMenuBar(menuBar);
+ }
+
+ /*
+ * Create the widgets in the main application window. The basic layout is a
+ * two-panel sash, with a scrolling list of VMs on the left and detailed
+ * output for a single VM on the right.
+ */
+ private void createWidgets(final Shell shell) {
+ Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+
+ /*
+ * Create three areas: tool bar, split panels, status line
+ */
+ shell.setLayout(new GridLayout(1, false));
+
+ // 1. panel area
+ final Composite panelArea = new Composite(shell, SWT.BORDER);
+
+ // make the panel area absorb all space
+ panelArea.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // 2. status line.
+ mStatusLine = new Label(shell, SWT.NONE);
+
+ // make status line extend all the way across
+ mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mStatusLine.setText("Initializing...");
+
+ /*
+ * Configure the split-panel area.
+ */
+ final PreferenceStore prefs = PrefsDialog.getStore();
+
+ Composite topPanel = new Composite(panelArea, SWT.NONE);
+ final Sash sash = new Sash(panelArea, SWT.HORIZONTAL);
+ sash.setBackground(darkGray);
+ Composite bottomPanel = new Composite(panelArea, SWT.NONE);
+
+ panelArea.setLayout(new FormLayout());
+
+ createTopPanel(topPanel, darkGray);
+
+ mClipboard = new Clipboard(panelArea.getDisplay());
+ if (useOldLogCatView()) {
+ createBottomPanel(bottomPanel);
+ } else {
+ createLogCatView(bottomPanel);
+ }
+
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ topPanel.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) {
+ sashData.top = new FormAttachment(0, prefs.getInt(
+ PREFERENCE_LOGSASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ bottomPanel.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = panelArea.getClientArea();
+ int bottom = panelRect.height - sashRect.height - 100;
+ e.y = Math.max(Math.min(e.y, bottom), 100);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ if (prefs != null) {
+ prefs.setValue(PREFERENCE_LOGSASH, e.y);
+ }
+ panelArea.layout();
+ }
+ }
+ });
+
+ // add a global focus listener for all the tables
+ mTableListener = new TableFocusListener();
+
+ // now set up the listener in the various panels
+ if (useOldLogCatView()) {
+ mLogPanel.setTableFocusListener(mTableListener);
+ } else {
+ mLogCatPanel.setTableFocusListener(mTableListener);
+ }
+ mEventLogPanel.setTableFocusListener(mTableListener);
+ for (TablePanel p : mPanels) {
+ if (p != null) {
+ p.setTableFocusListener(mTableListener);
+ }
+ }
+
+ mStatusLine.setText("");
+ }
+
+ /*
+ * Populate the tool bar.
+ */
+ private void createDevicePanelToolBar(ToolBar toolBar) {
+ Display display = toolBar.getDisplay();
+
+ // add "show heap updates" button
+ mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK);
+ mTBShowHeapUpdates.setImage(mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBShowHeapUpdates.setToolTipText("Show heap updates");
+ mTBShowHeapUpdates.setEnabled(false);
+ mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mCurrentClient != null) {
+ // boolean status = ((ToolItem)e.item).getSelection();
+ // invert previous state
+ boolean enable = !mCurrentClient.isHeapUpdateEnabled();
+ mCurrentClient.setHeapUpdateEnabled(enable);
+ } else {
+ e.doit = false; // this has no effect?
+ }
+ }
+ });
+
+ // add "dump HPROF" button
+ mTBDumpHprof = new ToolItem(toolBar, SWT.PUSH);
+ mTBDumpHprof.setToolTipText("Dump HPROF file");
+ mTBDumpHprof.setEnabled(false);
+ mTBDumpHprof.setImage(mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_HPROF, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBDumpHprof.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.dumpHprof();
+
+ // this will make sure the dump hprof button is disabled for the current selection
+ // as the client is already dumping an hprof file
+ enableButtons();
+ }
+ });
+
+ // add "cause GC" button
+ mTBCauseGc = new ToolItem(toolBar, SWT.PUSH);
+ mTBCauseGc.setToolTipText("Cause an immediate GC");
+ mTBCauseGc.setEnabled(false);
+ mTBCauseGc.setImage(mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBCauseGc.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.forceGcOnSelectedClient();
+ }
+ });
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ // add "show thread updates" button
+ mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK);
+ mTBShowThreadUpdates.setImage(mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBShowThreadUpdates.setToolTipText("Show thread updates");
+ mTBShowThreadUpdates.setEnabled(false);
+ mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mCurrentClient != null) {
+ // boolean status = ((ToolItem)e.item).getSelection();
+ // invert previous state
+ boolean enable = !mCurrentClient.isThreadUpdateEnabled();
+
+ mCurrentClient.setThreadUpdateEnabled(enable);
+ } else {
+ e.doit = false; // this has no effect?
+ }
+ }
+ });
+
+ // add a start/stop method tracing
+ mTracingStartImage = mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_TRACING_START,
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
+ mTracingStopImage = mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_TRACING_STOP,
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
+ mTBProfiling = new ToolItem(toolBar, SWT.PUSH);
+ mTBProfiling.setToolTipText("Start Method Profiling");
+ mTBProfiling.setEnabled(false);
+ mTBProfiling.setImage(mTracingStartImage);
+ mTBProfiling.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.toggleMethodProfiling();
+ }
+ });
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ // add "kill VM" button; need to make this visually distinct from
+ // the status update buttons
+ mTBHalt = new ToolItem(toolBar, SWT.PUSH);
+ mTBHalt.setToolTipText("Halt the target VM");
+ mTBHalt.setEnabled(false);
+ mTBHalt.setImage(mDdmUiLibLoader.loadImage(display,
+ DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mTBHalt.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDevicePanel.killSelectedClient();
+ }
+ });
+
+ toolBar.pack();
+ }
+
+ private void createTopPanel(final Composite comp, Color darkGray) {
+ final PreferenceStore prefs = PrefsDialog.getStore();
+
+ comp.setLayout(new FormLayout());
+
+ Composite leftPanel = new Composite(comp, SWT.NONE);
+ final Sash sash = new Sash(comp, SWT.VERTICAL);
+ sash.setBackground(darkGray);
+ Composite rightPanel = new Composite(comp, SWT.NONE);
+
+ createLeftPanel(leftPanel);
+ createRightPanel(rightPanel);
+
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(sash, 0);
+ leftPanel.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ sashData.top = new FormAttachment(0, 0);
+ sashData.bottom = new FormAttachment(100, 0);
+ if (prefs != null && prefs.contains(PREFERENCE_SASH)) {
+ sashData.left = new FormAttachment(0, prefs.getInt(
+ PREFERENCE_SASH));
+ } else {
+ // position the sash 380 from the right instead of x% (done by using
+ // FormAttachment(x, 0)) in order to keep the sash at the same
+ // position
+ // from the left when the window is resized.
+ // 380px is just enough to display the left table with no horizontal
+ // scrollbar with the default font.
+ sashData.left = new FormAttachment(0, 380);
+ }
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(sash, 0);
+ data.right = new FormAttachment(100, 0);
+ rightPanel.setLayoutData(data);
+
+ final int minPanelWidth = 60;
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = comp.getClientArea();
+ int right = panelRect.width - sashRect.width - minPanelWidth;
+ e.x = Math.max(Math.min(e.x, right), minPanelWidth);
+ if (e.x != sashRect.x) {
+ sashData.left = new FormAttachment(0, e.x);
+ if (prefs != null) {
+ prefs.setValue(PREFERENCE_SASH, e.x);
+ }
+ comp.layout();
+ }
+ }
+ });
+ }
+
+ private void createBottomPanel(final Composite comp) {
+ final PreferenceStore prefs = PrefsDialog.getStore();
+
+ // create clipboard
+ Display display = comp.getDisplay();
+
+ LogColors colors = new LogColors();
+
+ colors.infoColor = new Color(display, 0, 127, 0);
+ colors.debugColor = new Color(display, 0, 0, 127);
+ colors.errorColor = new Color(display, 255, 0, 0);
+ colors.warningColor = new Color(display, 255, 127, 0);
+ colors.verboseColor = new Color(display, 0, 0, 0);
+
+ // set the preferences names
+ LogPanel.PREFS_TIME = PREFS_COL_TIME;
+ LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL;
+ LogPanel.PREFS_PID = PREFS_COL_PID;
+ LogPanel.PREFS_TAG = PREFS_COL_TAG;
+ LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE;
+
+ comp.setLayout(new GridLayout(1, false));
+
+ ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL);
+
+ mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+ mCreateFilterAction.item.setToolTipText("Create Filter");
+ mCreateFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ "add.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mLogPanel.addFilter();
+ }
+ });
+
+ mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+ mEditFilterAction.item.setToolTipText("Edit Filter");
+ mEditFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ "edit.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mEditFilterAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mLogPanel.editFilter();
+ }
+ });
+
+ mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+ mDeleteFilterAction.item.setToolTipText("Delete Filter");
+ mDeleteFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ "delete.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mLogPanel.deleteFilter();
+ }
+ });
+
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ LogLevel[] levels = LogLevel.values();
+ mLogLevelActions = new ToolItemAction[mLogLevelIcons.length];
+ for (int i = 0 ; i < mLogLevelActions.length; i++) {
+ String name = levels[i].getStringValue();
+ final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK);
+ mLogLevelActions[i] = newAction;
+ //newAction.item.setText(name);
+ newAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // disable the other actions and record current index
+ for (int k = 0 ; k < mLogLevelActions.length; k++) {
+ ToolItemAction a = mLogLevelActions[k];
+ if (a == newAction) {
+ a.setChecked(true);
+
+ // set the log level
+ mLogPanel.setCurrentFilterLogLevel(k+2);
+ } else {
+ a.setChecked(false);
+ }
+ }
+ }
+ });
+
+ newAction.item.setToolTipText(name);
+ newAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ mLogLevelIcons[i],
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ }
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mClearAction = new ToolItemAction(toolBar, SWT.PUSH);
+ mClearAction.item.setToolTipText("Clear Log");
+
+ mClearAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ "clear.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mClearAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mLogPanel.clear();
+ }
+ });
+
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mExportAction = new ToolItemAction(toolBar, SWT.PUSH);
+ mExportAction.item.setToolTipText("Export Selection As Text...");
+ mExportAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+ "save.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+ mExportAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mLogPanel.save();
+ }
+ });
+
+
+ toolBar.pack();
+
+ // now create the log view
+ mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL);
+
+ mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions);
+
+ String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE);
+ if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) {
+ mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO);
+ }
+
+ String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT);
+ if (fontStr != null) {
+ try {
+ FontData fdat = new FontData(fontStr);
+ mLogPanel.setFont(new Font(display, fdat));
+ } catch (IllegalArgumentException e) {
+ // Looks like fontStr isn't a valid font representation.
+ // We do nothing in this case, the logcat view will use the default font.
+ } catch (SWTError e2) {
+ // Looks like the Font() constructor failed.
+ // We do nothing in this case, the logcat view will use the default font.
+ }
+ }
+
+ mLogPanel.createPanel(comp);
+
+ // and start the logcat
+ mLogPanel.startLogCat(mCurrentDevice);
+ }
+
+ private void createLogCatView(Composite parent) {
+ IPreferenceStore prefStore = DdmUiPreferences.getStore();
+ mLogCatPanel = new LogCatPanel(prefStore);
+ mLogCatPanel.createPanel(parent);
+
+ if (mCurrentDevice != null) {
+ mLogCatPanel.deviceSelected(mCurrentDevice);
+ }
+ }
+
+ /*
+ * Create the contents of the left panel: a table of VMs.
+ */
+ private void createLeftPanel(final Composite comp) {
+ comp.setLayout(new GridLayout(1, false));
+ ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP);
+ toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ createDevicePanelToolBar(toolBar);
+
+ Composite c = new Composite(comp, SWT.NONE);
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mDevicePanel = new DevicePanel(true /* showPorts */);
+ mDevicePanel.createPanel(c);
+
+ // add ourselves to the device panel selection listener
+ mDevicePanel.addSelectionListener(this);
+ }
+
+ /*
+ * Create the contents of the right panel: tabs with VM information.
+ */
+ private void createRightPanel(final Composite comp) {
+ TabItem item;
+ TabFolder tabFolder;
+
+ comp.setLayout(new FillLayout());
+
+ tabFolder = new TabFolder(comp, SWT.NONE);
+
+ for (int i = 0; i < mPanels.length; i++) {
+ if (mPanels[i] != null) {
+ item = new TabItem(tabFolder, SWT.NONE);
+ item.setText(mPanelNames[i]);
+ item.setToolTipText(mPanelTips[i]);
+ item.setControl(mPanels[i].createPanel(tabFolder));
+ }
+ }
+
+ // add the emulator control panel to the folders.
+ item = new TabItem(tabFolder, SWT.NONE);
+ item.setText("Emulator Control");
+ item.setToolTipText("Emulator Control Panel");
+ mEmulatorPanel = new EmulatorControlPanel();
+ item.setControl(mEmulatorPanel.createPanel(tabFolder));
+
+ // add the event log panel to the folders.
+ item = new TabItem(tabFolder, SWT.NONE);
+ item.setText("Event Log");
+ item.setToolTipText("Event Log");
+
+ // create the composite that will hold the toolbar and the event log panel.
+ Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE);
+ item.setControl(eventLogTopComposite);
+ eventLogTopComposite.setLayout(new GridLayout(1, false));
+
+ // create the toolbar and the actions
+ ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL);
+ toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH);
+ optionsAction.item.setToolTipText("Opens the options panel");
+ optionsAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+ "edit.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH);
+ clearAction.item.setToolTipText("Clears the event log");
+ clearAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+ "clear.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ new ToolItem(toolbar, SWT.SEPARATOR);
+
+ ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH);
+ saveAction.item.setToolTipText("Saves the event log");
+ saveAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+ "save.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH);
+ loadAction.item.setToolTipText("Loads an event log");
+ loadAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+ "load.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH);
+ importBugAction.item.setToolTipText("Imports a bug report");
+ importBugAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+ "importBug.png", //$NON-NLS-1$
+ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+ // create the event log panel
+ mEventLogPanel = new EventLogPanel();
+
+ // set the external actions
+ mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction,
+ importBugAction);
+
+ // create the panel
+ mEventLogPanel.createPanel(eventLogTopComposite);
+ }
+
+ private void createFileExplorer() {
+ if (mExplorer == null) {
+ mExplorerShell = new Shell(mDisplay);
+
+ // create the ui
+ mExplorerShell.setLayout(new GridLayout(1, false));
+
+ // toolbar + action
+ ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL);
+ toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH);
+ pullAction.item.setToolTipText("Pull File from Device");
+ Image image = mDdmUiLibLoader.loadImage("pull.png", mDisplay); //$NON-NLS-1$
+ if (image != null) {
+ pullAction.item.setImage(image);
+ } else {
+ // this is for debugging purpose when the icon is missing
+ pullAction.item.setText("Pull"); //$NON-NLS-1$
+ }
+
+ ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH);
+ pushAction.item.setToolTipText("Push file onto Device");
+ image = mDdmUiLibLoader.loadImage("push.png", mDisplay); //$NON-NLS-1$
+ if (image != null) {
+ pushAction.item.setImage(image);
+ } else {
+ // this is for debugging purpose when the icon is missing
+ pushAction.item.setText("Push"); //$NON-NLS-1$
+ }
+
+ ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH);
+ deleteAction.item.setToolTipText("Delete");
+ image = mDdmUiLibLoader.loadImage("delete.png", mDisplay); //$NON-NLS-1$
+ if (image != null) {
+ deleteAction.item.setImage(image);
+ } else {
+ // this is for debugging purpose when the icon is missing
+ deleteAction.item.setText("Delete"); //$NON-NLS-1$
+ }
+
+ ToolItemAction createNewFolderAction = new ToolItemAction(toolBar, SWT.PUSH);
+ createNewFolderAction.item.setToolTipText("New Folder");
+ image = mDdmUiLibLoader.loadImage("add.png", mDisplay); //$NON-NLS-1$
+ if (image != null) {
+ createNewFolderAction.item.setImage(image);
+ } else {
+ // this is for debugging purpose when the icon is missing
+ createNewFolderAction.item.setText("New Folder"); //$NON-NLS-1$
+ }
+
+ // device explorer
+ mExplorer = new DeviceExplorer();
+ mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction);
+
+ pullAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mExplorer.pullSelection();
+ }
+ });
+ pullAction.setEnabled(false);
+
+ pushAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mExplorer.pushIntoSelection();
+ }
+ });
+ pushAction.setEnabled(false);
+
+ deleteAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mExplorer.deleteSelection();
+ }
+ });
+ deleteAction.setEnabled(false);
+
+ createNewFolderAction.item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mExplorer.createNewFolderInSelection();
+ }
+ });
+ createNewFolderAction.setEnabled(false);
+
+ Composite parent = new Composite(mExplorerShell, SWT.NONE);
+ parent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mExplorer.createPanel(parent);
+ mExplorer.switchDevice(mCurrentDevice);
+
+ mExplorerShell.addShellListener(new ShellListener() {
+ @Override
+ public void shellActivated(ShellEvent e) {
+ // pass
+ }
+
+ @Override
+ public void shellClosed(ShellEvent e) {
+ mExplorer = null;
+ mExplorerShell = null;
+ }
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ // pass
+ }
+
+ @Override
+ public void shellDeiconified(ShellEvent e) {
+ // pass
+ }
+
+ @Override
+ public void shellIconified(ShellEvent e) {
+ // pass
+ }
+ });
+
+ mExplorerShell.pack();
+ setExplorerSizeAndPosition(mExplorerShell);
+ mExplorerShell.open();
+ } else {
+ if (mExplorerShell != null) {
+ mExplorerShell.forceActive();
+ }
+ }
+ }
+
+ /**
+ * Set the status line. TODO: make this a stack, so we can safely have
+ * multiple things trying to set it all at once. Also specify an expiration?
+ */
+ public void setStatusLine(final String str) {
+ try {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ doSetStatusLine(str);
+ }
+ });
+ } catch (SWTException swte) {
+ if (!mDisplay.isDisposed())
+ throw swte;
+ }
+ }
+
+ private void doSetStatusLine(String str) {
+ if (mStatusLine.isDisposed())
+ return;
+
+ if (!mStatusLine.getText().equals(str)) {
+ mStatusLine.setText(str);
+
+ // try { Thread.sleep(100); }
+ // catch (InterruptedException ie) {}
+ }
+ }
+
+ public void displayError(final String msg) {
+ try {
+ mDisplay.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ MessageDialog.openError(mDisplay.getActiveShell(), "Error",
+ msg);
+ }
+ });
+ } catch (SWTException swte) {
+ if (!mDisplay.isDisposed())
+ throw swte;
+ }
+ }
+
+ private void enableButtons() {
+ if (mCurrentClient != null) {
+ mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled());
+ mTBShowThreadUpdates.setEnabled(true);
+ mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled());
+ mTBShowHeapUpdates.setEnabled(true);
+ mTBHalt.setEnabled(true);
+ mTBCauseGc.setEnabled(true);
+
+ ClientData data = mCurrentClient.getClientData();
+
+ if (data.hasFeature(ClientData.FEATURE_HPROF)) {
+ mTBDumpHprof.setEnabled(data.hasPendingHprofDump() == false);
+ mTBDumpHprof.setToolTipText("Dump HPROF file");
+ } else {
+ mTBDumpHprof.setEnabled(false);
+ mTBDumpHprof.setToolTipText("Dump HPROF file (not supported by this VM)");
+ }
+
+ if (data.hasFeature(ClientData.FEATURE_PROFILING)) {
+ mTBProfiling.setEnabled(true);
+ if (data.getMethodProfilingStatus() == MethodProfilingStatus.ON) {
+ mTBProfiling.setToolTipText("Stop Method Profiling");
+ mTBProfiling.setImage(mTracingStopImage);
+ } else {
+ mTBProfiling.setToolTipText("Start Method Profiling");
+ mTBProfiling.setImage(mTracingStartImage);
+ }
+ } else {
+ mTBProfiling.setEnabled(false);
+ mTBProfiling.setImage(mTracingStartImage);
+ mTBProfiling.setToolTipText("Start Method Profiling (not supported by this VM)");
+ }
+ } else {
+ // list is empty, disable these
+ mTBShowThreadUpdates.setSelection(false);
+ mTBShowThreadUpdates.setEnabled(false);
+ mTBShowHeapUpdates.setSelection(false);
+ mTBShowHeapUpdates.setEnabled(false);
+ mTBHalt.setEnabled(false);
+ mTBCauseGc.setEnabled(false);
+
+ mTBDumpHprof.setEnabled(false);
+ mTBDumpHprof.setToolTipText("Dump HPROF file");
+
+ mTBProfiling.setEnabled(false);
+ mTBProfiling.setImage(mTracingStartImage);
+ mTBProfiling.setToolTipText("Start Method Profiling");
+ }
+ }
+
+ /**
+ * Sent when a new {@link IDevice} and {@link Client} are selected.
+ * @param selectedDevice the selected device. If null, no devices are selected.
+ * @param selectedClient The selected client. If null, no clients are selected.
+ *
+ * @see IUiSelectionListener
+ */
+ @Override
+ public void selectionChanged(IDevice selectedDevice, Client selectedClient) {
+ if (mCurrentDevice != selectedDevice) {
+ mCurrentDevice = selectedDevice;
+ for (TablePanel panel : mPanels) {
+ if (panel != null) {
+ panel.deviceSelected(mCurrentDevice);
+ }
+ }
+
+ mEmulatorPanel.deviceSelected(mCurrentDevice);
+ if (useOldLogCatView()) {
+ mLogPanel.deviceSelected(mCurrentDevice);
+ } else {
+ mLogCatPanel.deviceSelected(mCurrentDevice);
+ }
+ if (mEventLogPanel != null) {
+ mEventLogPanel.deviceSelected(mCurrentDevice);
+ }
+
+ if (mExplorer != null) {
+ mExplorer.switchDevice(mCurrentDevice);
+ }
+ }
+
+ if (mCurrentClient != selectedClient) {
+ AndroidDebugBridge.getBridge().setSelectedClient(selectedClient);
+ mCurrentClient = selectedClient;
+ for (TablePanel panel : mPanels) {
+ if (panel != null) {
+ panel.clientSelected(mCurrentClient);
+ }
+ }
+
+ enableButtons();
+ }
+ }
+
+ @Override
+ public void clientChanged(Client client, int changeMask) {
+ if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) ==
+ Client.CHANGE_METHOD_PROFILING_STATUS) {
+ if (mCurrentClient == client) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ // force refresh of the button enabled state.
+ enableButtons();
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/ddms/app/src/main/java/images/ddms-128.png b/ddms/app/src/main/java/images/ddms-128.png
new file mode 100644
index 0000000..392a8f3
--- /dev/null
+++ b/ddms/app/src/main/java/images/ddms-128.png
Binary files differ
diff --git a/ddms/ddmuilib/.classpath b/ddms/ddmuilib/.classpath
new file mode 100644
index 0000000..829ac1c
--- /dev/null
+++ b/ddms/ddmuilib/.classpath
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src/main/java"/>
+ <classpathentry kind="src" path="src/test/java"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+ <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.core.commands_3.6.0.I20100512-1500.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.equinox.common_3.6.0.v20100503.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.jface_3.6.2.M20110210-1200.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jcommon-1.0.12.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jfreechart-1.0.9-swt.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jfreechart-1.0.9.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/ddmuilib/.project b/ddms/ddmuilib/.project
new file mode 100644
index 0000000..29cb2f2
--- /dev/null
+++ b/ddms/ddmuilib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ddmuilib</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/ddms/ddmuilib/NOTICE b/ddms/ddmuilib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/ddms/ddmuilib/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/ddms/ddmuilib/README b/ddms/ddmuilib/README
new file mode 100644
index 0000000..971e211
--- /dev/null
+++ b/ddms/ddmuilib/README
@@ -0,0 +1,14 @@
+Using the Eclipse projects for ddmuilib.
+
+ddmuilib requires SWT to compile.
+
+SWT is available in the depot under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside the project directory,
+the .classpath file references a user library called ANDROID_SWT.
+
+In order to compile the project, make a user library called ANDROID_SWT containing the jar files
+available at prebuild/<platform>/swt.
+
+You also need a user library called ANDROID_JFREECHART containing the jar files
+available at prebuild/common/jfreechart.
diff --git a/ddms/ddmuilib/build.gradle b/ddms/ddmuilib/build.gradle
new file mode 100644
index 0000000..f565e49
--- /dev/null
+++ b/ddms/ddmuilib/build.gradle
@@ -0,0 +1,14 @@
+group = 'com.android.tools.ddms'
+archivesBaseName = 'ddmuilib'
+
+dependencies {
+ compile "com.android.tools.ddms:ddmlib:$version"
+ compile 'jfree:jfreechart:1.0.9'
+ compile 'jfree:jfreechart-swt:1.0.9'
+
+ testCompile 'junit:junit:3.8.1'
+}
+
+// include swt for compilation
+sourceSets.main.compileClasspath += configurations.swt
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java
new file mode 100644
index 0000000..13a787a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import java.util.regex.Pattern;
+
+/**
+ * {@link AbstractBufferFindTarget} implements methods to find items inside a buffer. It takes
+ * care of the logic to search backwards/forwards in the buffer, wrapping around when necessary.
+ * The actual contents of the buffer should be provided by the classes that extend this.
+ */
+public abstract class AbstractBufferFindTarget implements IFindTarget {
+ private int mCurrentSearchIndex;
+
+ // Single element cache of the last search regex
+ private Pattern mLastSearchPattern;
+ private String mLastSearchText;
+
+ @Override
+ public boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward) {
+ boolean found = false;
+ int maxIndex = getItemCount();
+
+ synchronized (this) {
+ // Find starting index for this search
+ if (isNewSearch) {
+ // for new searches, start from an appropriate place as provided by the delegate
+ mCurrentSearchIndex = getStartingIndex();
+ } else {
+ // for ongoing searches (finding next match for the same term), continue from
+ // the current result index
+ mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex);
+ }
+
+ // Create a regex pattern based on the search term.
+ Pattern pattern;
+ if (text.equals(mLastSearchText)) {
+ pattern = mLastSearchPattern;
+ } else {
+ pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+ mLastSearchPattern = pattern;
+ mLastSearchText = text;
+ }
+
+ // Iterate through the list of items. The search ends if we have gone through
+ // all items once.
+ int index = mCurrentSearchIndex;
+ do {
+ String msgText = getItem(mCurrentSearchIndex);
+ if (msgText != null && pattern.matcher(msgText).find()) {
+ found = true;
+ break;
+ }
+
+ mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex);
+ } while (index != mCurrentSearchIndex); // loop through entire contents once
+ }
+
+ if (found) {
+ selectAndReveal(mCurrentSearchIndex);
+ }
+
+ return found;
+ }
+
+ /** Indicate that the log buffer has scrolled by certain number of elements */
+ public void scrollBy(int delta) {
+ synchronized (this) {
+ if (mCurrentSearchIndex > 0) {
+ mCurrentSearchIndex = Math.max(0, mCurrentSearchIndex - delta);
+ }
+ }
+ }
+
+ private int getNext(int index, boolean searchForward, int max) {
+ // increment or decrement index
+ index = searchForward ? index + 1 : index - 1;
+
+ // take care of underflow
+ if (index == -1) {
+ index = max - 1;
+ }
+
+ // ..and overflow
+ if (index == max) {
+ index = 0;
+ }
+
+ return index;
+ }
+
+ /** Obtain the number of items in the buffer */
+ public abstract int getItemCount();
+
+ /** Obtain the item at given index */
+ public abstract String getItem(int index);
+
+ /** Select and reveal the item at given index */
+ public abstract void selectAndReveal(int index);
+
+ /** Obtain the index from which search should begin */
+ public abstract int getStartingIndex();
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java
new file mode 100644
index 0000000..10799ec
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Represents an addr2line process to get filename/method information from a
+ * memory address.<br>
+ * Each process can only handle one library, which should be provided when
+ * creating a new process.<br>
+ * <br>
+ * The processes take some time to load as they need to parse the library files.
+ * For this reason, processes cannot be manually started. Instead the class
+ * keeps an internal list of processes and one asks for a process for a specific
+ * library, using <code>getProcess(String library)<code>.<br></br>
+ * Internally, the processes are started in pipe mode to be able to query them
+ * with multiple addresses.
+ */
+public class Addr2Line {
+ private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS";
+
+ private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT =
+ "Unable to locate library %s on disk. Addresses mapping to this library "
+ + "will not be resolved. In order to fix this, set the the library search path "
+ + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + ".";
+
+ /**
+ * Loaded processes list. This is also used as a locking object for any
+ * methods dealing with starting/stopping/creating processes/querying for
+ * method.
+ */
+ private static final HashMap<String, Addr2Line> sProcessCache =
+ new HashMap<String, Addr2Line>();
+
+ /**
+ * byte array representing a carriage return. Used to push addresses in the
+ * process pipes.
+ */
+ private static final byte[] sCrLf = {
+ '\n'
+ };
+
+ /** Path to the library */
+ private NativeLibraryMapInfo mLibrary;
+
+ /** the command line process */
+ private Process mProcess;
+
+ /** buffer to read the result of the command line process from */
+ private BufferedReader mResultReader;
+
+ /**
+ * output stream to provide new addresses to decode to the command line
+ * process
+ */
+ private BufferedOutputStream mAddressWriter;
+
+ private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER;
+ static {
+ String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR);
+ if (symbols == null) {
+ DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory();
+ } else {
+ DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols;
+ }
+ }
+
+ private static List<String> mLibrarySearchPaths = new ArrayList<String>();
+
+ /**
+ * Set the search path where libraries should be found.
+ * @param path search path to use, can be a colon separated list of paths if multiple folders
+ * should be searched
+ */
+ public static void setSearchPath(String path) {
+ mLibrarySearchPaths.clear();
+ mLibrarySearchPaths.addAll(Arrays.asList(path.split(":")));
+ }
+
+ /**
+ * Returns the instance of a Addr2Line process for the specified library.
+ * <br>The library should be in a format that makes<br>
+ * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
+ *
+ * @param library the library in which to look for addresses.
+ * @return a new Addr2Line object representing a started process, ready to
+ * be queried for addresses. If any error happened when launching a
+ * new process, <code>null</code> will be returned.
+ */
+ public static Addr2Line getProcess(final NativeLibraryMapInfo library) {
+ String libName = library.getLibraryName();
+
+ // synchronize around the hashmap object
+ if (libName != null) {
+ synchronized (sProcessCache) {
+ // look for an existing process
+ Addr2Line process = sProcessCache.get(libName);
+
+ // if we don't find one, we create it
+ if (process == null) {
+ process = new Addr2Line(library);
+
+ // then we start it
+ boolean status = process.start();
+
+ if (status) {
+ // if starting the process worked, then we add it to the
+ // list.
+ sProcessCache.put(libName, process);
+ } else {
+ // otherwise we just drop the object, to return null
+ process = null;
+ }
+ }
+ // return the process
+ return process;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Construct the object with a library name. The library should be present
+ * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user
+ * provided search path.
+ *
+ * @param library the library in which to look for address.
+ */
+ private Addr2Line(final NativeLibraryMapInfo library) {
+ mLibrary = library;
+ }
+
+ /**
+ * Search for the library in the library search path and obtain the full path to where it
+ * is found.
+ * @return fully resolved path to the library if found in search path, null otherwise
+ */
+ private String getLibraryPath(String library) {
+ // first check the symbols folder
+ String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library;
+ if (new File(path).exists()) {
+ return path;
+ }
+
+ for (String p : mLibrarySearchPaths) {
+ // try appending the full path on device
+ String fullPath = p + "/" + library;
+ if (new File(fullPath).exists()) {
+ return fullPath;
+ }
+
+ // try appending basename(library)
+ fullPath = p + "/" + new File(library).getName();
+ if (new File(fullPath).exists()) {
+ return fullPath;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Starts the command line process.
+ *
+ * @return true if the process was started, false if it failed to start, or
+ * if there was any other errors.
+ */
+ private boolean start() {
+ // because this is only called from getProcess() we know we don't need
+ // to synchronize this code.
+
+ String addr2Line = System.getenv("ANDROID_ADDR2LINE");
+ if (addr2Line == null) {
+ addr2Line = DdmUiPreferences.getAddr2Line();
+ }
+
+ // build the command line
+ String[] command = new String[5];
+ command[0] = addr2Line;
+ command[1] = "-C";
+ command[2] = "-f";
+ command[3] = "-e";
+
+ String fullPath = getLibraryPath(mLibrary.getLibraryName());
+ if (fullPath == null) {
+ String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName());
+ Log.e("ddm-Addr2Line", msg);
+ return false;
+ }
+
+ command[4] = fullPath;
+
+ try {
+ // attempt to start the process
+ mProcess = Runtime.getRuntime().exec(command);
+
+ if (mProcess != null) {
+ // get the result reader
+ InputStreamReader is = new InputStreamReader(mProcess
+ .getInputStream());
+ mResultReader = new BufferedReader(is);
+
+ // get the outstream to write the addresses
+ mAddressWriter = new BufferedOutputStream(mProcess
+ .getOutputStream());
+
+ // check our streams are here
+ if (mResultReader == null || mAddressWriter == null) {
+ // not here? stop the process and return false;
+ mProcess.destroy();
+ mProcess = null;
+ return false;
+ }
+
+ // return a success
+ return true;
+ }
+
+ } catch (IOException e) {
+ // log the error
+ String msg = String.format(
+ "Error while trying to start %1$s process for library %2$s",
+ DdmUiPreferences.getAddr2Line(), mLibrary);
+ Log.e("ddm-Addr2Line", msg);
+
+ // drop the process just in case
+ if (mProcess != null) {
+ mProcess.destroy();
+ mProcess = null;
+ }
+ }
+
+ // we can be here either cause the allocation of mProcess failed, or we
+ // caught an exception
+ return false;
+ }
+
+ /**
+ * Stops the command line process.
+ */
+ public void stop() {
+ synchronized (sProcessCache) {
+ if (mProcess != null) {
+ // remove the process from the list
+ sProcessCache.remove(mLibrary);
+
+ // then stops the process
+ mProcess.destroy();
+
+ // set the reference to null.
+ // this allows to make sure another thread calling getAddress()
+ // will not query a stopped thread
+ mProcess = null;
+ }
+ }
+ }
+
+ /**
+ * Stops all current running processes.
+ */
+ public static void stopAll() {
+ // because of concurrent access (and our use of HashMap.values()), we
+ // can't rely on the synchronized inside stop(). We need to put one
+ // around the whole loop.
+ synchronized (sProcessCache) {
+ // just a basic loop on all the values in the hashmap and call to
+ // stop();
+ Collection<Addr2Line> col = sProcessCache.values();
+ for (Addr2Line a2l : col) {
+ a2l.stop();
+ }
+ }
+ }
+
+ /**
+ * Looks up an address and returns method name, source file name, and line
+ * number.
+ *
+ * @param addr the address to look up
+ * @return a BacktraceInfo object containing the method/filename/linenumber
+ * or null if the process we stopped before the query could be
+ * processed, or if an IO exception happened.
+ */
+ public NativeStackCallInfo getAddress(long addr) {
+ long offset = addr - mLibrary.getStartAddress();
+
+ // even though we don't access the hashmap object, we need to
+ // synchronized on it to prevent
+ // another thread from stopping the process we're going to query.
+ synchronized (sProcessCache) {
+ // check the process is still alive/allocated
+ if (mProcess != null) {
+ // prepare to the write the address to the output buffer.
+
+ // first, conversion to a string containing the hex value.
+ String tmp = Long.toString(offset, 16);
+
+ try {
+ // write the address to the buffer
+ mAddressWriter.write(tmp.getBytes());
+
+ // add CR-LF
+ mAddressWriter.write(sCrLf);
+
+ // flush it all.
+ mAddressWriter.flush();
+
+ // read the result. We need to read 2 lines
+ String method = mResultReader.readLine();
+ String source = mResultReader.readLine();
+
+ // make the backtrace object and return it
+ if (method != null && source != null) {
+ return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source);
+ }
+ } catch (IOException e) {
+ // log the error
+ Log.e("ddms",
+ "Error while trying to get information for addr: "
+ + tmp + " in library: " + mLibrary);
+ // we'll return null later
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
new file mode 100644
index 0000000..a48f73d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AllocationInfo;
+import com.android.ddmlib.AllocationInfo.AllocationSorter;
+import com.android.ddmlib.AllocationInfo.SortMode;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData.AllocationTrackingStatus;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Base class for our information panels.
+ */
+public class AllocationPanel extends TablePanel {
+
+ private final static String PREFS_ALLOC_COL_NUMBER = "allocPanel.Col00"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$
+
+ private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$
+
+ private static final String PREFS_STACK_COLUMN = "allocPanel.stack.col0"; //$NON-NLS-1$
+
+ private Composite mAllocationBase;
+ private Table mAllocationTable;
+ private TableViewer mAllocationViewer;
+
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+ private Button mEnableButton;
+ private Button mRequestButton;
+ private Button mTraceFilterCheck;
+
+ private final AllocationSorter mSorter = new AllocationSorter();
+ private TableColumn mSortColumn;
+ private Image mSortUpImg;
+ private Image mSortDownImg;
+ private String mFilterText = null;
+
+ /**
+ * Content Provider to display the allocations of a client.
+ * Expected input is a {@link Client} object, elements used in the table are of type
+ * {@link AllocationInfo}.
+ */
+ private class AllocationContentProvider implements IStructuredContentProvider {
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations();
+ if (allocs != null) {
+ if (mFilterText != null && mFilterText.length() > 0) {
+ allocs = getFilteredAllocations(allocs, mFilterText);
+ }
+ Arrays.sort(allocs, mSorter);
+ return allocs;
+ }
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be
+ * of type {@link AllocationInfo}.
+ */
+ private static class AllocationLabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof AllocationInfo) {
+ AllocationInfo alloc = (AllocationInfo)element;
+ switch (columnIndex) {
+ case 0:
+ return Integer.toString(alloc.getAllocNumber());
+ case 1:
+ return Integer.toString(alloc.getSize());
+ case 2:
+ return alloc.getAllocatedClass();
+ case 3:
+ return Short.toString(alloc.getThreadId());
+ case 4:
+ return alloc.getFirstTraceClassName();
+ case 5:
+ return alloc.getFirstTraceMethodName();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ Display display = parent.getDisplay();
+
+ // get some images
+ mSortUpImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_up.png", display);
+ mSortDownImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_down.png", display);
+
+ // base composite for selected client with enabled thread update.
+ mAllocationBase = new Composite(parent, SWT.NONE);
+ mAllocationBase.setLayout(new FormLayout());
+
+ // table above the sash
+ Composite topParent = new Composite(mAllocationBase, SWT.NONE);
+ topParent.setLayout(new GridLayout(6, false));
+
+ mEnableButton = new Button(topParent, SWT.PUSH);
+ mEnableButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Client current = getCurrentClient();
+ AllocationTrackingStatus status = current.getClientData().getAllocationStatus();
+ if (status == AllocationTrackingStatus.ON) {
+ current.enableAllocationTracker(false);
+ } else {
+ current.enableAllocationTracker(true);
+ }
+ current.requestAllocationStatus();
+ }
+ });
+
+ mRequestButton = new Button(topParent, SWT.PUSH);
+ mRequestButton.setText("Get Allocations");
+ mRequestButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ getCurrentClient().requestAllocationDetails();
+ }
+ });
+
+ setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF);
+
+ GridData gridData;
+
+ Composite spacer = new Composite(topParent, SWT.NONE);
+ spacer.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(topParent, SWT.NONE).setText("Filter:");
+
+ final Text filterText = new Text(topParent, SWT.BORDER);
+ filterText.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL));
+ gridData.widthHint = 200;
+
+ filterText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ mFilterText = filterText.getText().trim();
+ mAllocationViewer.refresh();
+ }
+ });
+
+ mTraceFilterCheck = new Button(topParent, SWT.CHECK);
+ mTraceFilterCheck.setText("Inc. trace");
+ mTraceFilterCheck.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ mAllocationViewer.refresh();
+ }
+ });
+
+ mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION);
+ mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH));
+ gridData.horizontalSpan = 6;
+ mAllocationTable.setHeaderVisible(true);
+ mAllocationTable.setLinesVisible(true);
+
+ final TableColumn numberCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Alloc Order",
+ SWT.RIGHT,
+ "Alloc Order", //$NON-NLS-1$
+ PREFS_ALLOC_COL_NUMBER, store);
+ numberCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(numberCol, SortMode.NUMBER);
+ }
+ });
+
+ final TableColumn sizeCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocation Size",
+ SWT.RIGHT,
+ "888", //$NON-NLS-1$
+ PREFS_ALLOC_COL_SIZE, store);
+ sizeCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(sizeCol, SortMode.SIZE);
+ }
+ });
+
+ final TableColumn classCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated Class",
+ SWT.LEFT,
+ "Allocated Class", //$NON-NLS-1$
+ PREFS_ALLOC_COL_CLASS, store);
+ classCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(classCol, SortMode.CLASS);
+ }
+ });
+
+ final TableColumn threadCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Thread Id",
+ SWT.LEFT,
+ "999", //$NON-NLS-1$
+ PREFS_ALLOC_COL_THREAD, store);
+ threadCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(threadCol, SortMode.THREAD);
+ }
+ });
+
+ final TableColumn inClassCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_CLASS, store);
+ inClassCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(inClassCol, SortMode.IN_CLASS);
+ }
+ });
+
+ final TableColumn inMethodCol = TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_METHOD, store);
+ inMethodCol.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ setSortColumn(inMethodCol, SortMode.IN_METHOD);
+ }
+ });
+
+ // init the default sort colum
+ switch (mSorter.getSortMode()) {
+ case SIZE:
+ mSortColumn = sizeCol;
+ break;
+ case CLASS:
+ mSortColumn = classCol;
+ break;
+ case THREAD:
+ mSortColumn = threadCol;
+ break;
+ case IN_CLASS:
+ mSortColumn = inClassCol;
+ break;
+ case IN_METHOD:
+ mSortColumn = inMethodCol;
+ break;
+ }
+
+ mSortColumn.setImage(mSorter.isDescending() ? mSortDownImg : mSortUpImg);
+
+ mAllocationViewer = new TableViewer(mAllocationTable);
+ mAllocationViewer.setContentProvider(new AllocationContentProvider());
+ mAllocationViewer.setLabelProvider(new AllocationLabelProvider());
+
+ mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection());
+ updateAllocationStackTrace(selectedAlloc);
+ }
+ });
+
+ // the separating sash
+ final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL);
+ Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+ sash.setBackground(darkGray);
+
+ // the UI below the sash
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, PREFS_STACK_COLUMN, store);
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ topParent.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_ALLOC_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = mAllocationBase.getClientArea();
+ int bottom = panelRect.height - sashRect.height - 100;
+ e.y = Math.max(Math.min(e.y, bottom), 100);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ store.setValue(PREFS_ALLOC_SASH, e.y);
+ mAllocationBase.layout();
+ }
+ }
+ });
+
+ return mAllocationBase;
+ }
+
+ @Override
+ public void dispose() {
+ mSortUpImg.dispose();
+ mSortDownImg.dispose();
+ super.dispose();
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mAllocationTable.setFocus();
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) {
+ try {
+ mAllocationTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mAllocationViewer.refresh();
+ updateAllocationStackCall();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) {
+ try {
+ mAllocationTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ setUpButtons(true, client.getClientData().getAllocationStatus());
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mAllocationTable.isDisposed()) {
+ return;
+ }
+
+ Client client = getCurrentClient();
+
+ mStackTracePanel.setCurrentClient(client);
+ mStackTracePanel.setViewerInput(null); // always empty on client selection change.
+
+ if (client != null) {
+ setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus());
+ } else {
+ setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF);
+ }
+
+ mAllocationViewer.setInput(client);
+ }
+
+ /**
+ * Updates the stack call of the currently selected thread.
+ * <p/>
+ * This <b>must</b> be called from the UI thread.
+ */
+ private void updateAllocationStackCall() {
+ Client client = getCurrentClient();
+ if (client != null) {
+ // get the current selection in the ThreadTable
+ AllocationInfo selectedAlloc = getAllocationSelection(null);
+
+ if (selectedAlloc != null) {
+ updateAllocationStackTrace(selectedAlloc);
+ } else {
+ updateAllocationStackTrace(null);
+ }
+ }
+ }
+
+ /**
+ * updates the stackcall of the specified allocation. If <code>null</code> the UI is emptied
+ * of current data.
+ * @param thread
+ */
+ private void updateAllocationStackTrace(AllocationInfo alloc) {
+ mStackTracePanel.setViewerInput(alloc);
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mAllocationTable);
+ addTableToFocusListener(mStackTraceTable);
+ }
+
+ /**
+ * Returns the current allocation selection or <code>null</code> if none is found.
+ * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this
+ * selection is returned, otherwise, the <code>ISelection</code> returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or <code>null</code>
+ */
+ private AllocationInfo getAllocationSelection(ISelection selection) {
+ if (selection == null) {
+ selection = mAllocationViewer.getSelection();
+ }
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof AllocationInfo) {
+ return (AllocationInfo)object;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ *
+ * @param enabled
+ * @param trackingStatus
+ */
+ private void setUpButtons(boolean enabled, AllocationTrackingStatus trackingStatus) {
+ if (enabled) {
+ switch (trackingStatus) {
+ case UNKNOWN:
+ mEnableButton.setText("?");
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ break;
+ case OFF:
+ mEnableButton.setText("Start Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(false);
+ break;
+ case ON:
+ mEnableButton.setText("Stop Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(true);
+ break;
+ }
+ } else {
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ mEnableButton.setText("Start Tracking");
+ }
+ }
+
+ private void setSortColumn(final TableColumn column, SortMode sortMode) {
+ // set the new sort mode
+ mSorter.setSortMode(sortMode);
+
+ mAllocationTable.setRedraw(false);
+
+ // remove image from previous sort colum
+ if (mSortColumn != column) {
+ mSortColumn.setImage(null);
+ }
+
+ mSortColumn = column;
+ if (mSorter.isDescending()) {
+ mSortColumn.setImage(mSortDownImg);
+ } else {
+ mSortColumn.setImage(mSortUpImg);
+ }
+
+ mAllocationTable.setRedraw(true);
+ mAllocationViewer.refresh();
+ }
+
+ private AllocationInfo[] getFilteredAllocations(AllocationInfo[] allocations,
+ String filterText) {
+ ArrayList<AllocationInfo> results = new ArrayList<AllocationInfo>();
+ // Using default locale here such that the locale-specific c
+ Locale locale = Locale.getDefault();
+ filterText = filterText.toLowerCase(locale);
+ boolean fullTrace = mTraceFilterCheck.getSelection();
+
+ for (AllocationInfo info : allocations) {
+ if (info.filter(filterText, fullTrace, locale)) {
+ results.add(info);
+ }
+ }
+
+ return results.toArray(new AllocationInfo[results.size()]);
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java
new file mode 100644
index 0000000..0ed4c95
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+/**
+ * base background thread class. The class provides a synchronous quit method
+ * which sets a quitting flag to true. Inheriting classes should regularly test
+ * this flag with <code>isQuitting()</code> and should finish if the flag is
+ * true.
+ */
+public abstract class BackgroundThread extends Thread {
+ private boolean mQuit = false;
+
+ /**
+ * Tell the thread to exit. This is usually called from the UI thread. The
+ * call is synchronous and will only return once the thread has terminated
+ * itself.
+ */
+ public final void quit() {
+ mQuit = true;
+ Log.d("ddms", "Waiting for BackgroundThread to quit");
+ try {
+ this.join();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ /** returns if the thread was asked to quit. */
+ protected final boolean isQuitting() {
+ return mQuit;
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java
new file mode 100644
index 0000000..3e66ea5
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.HeapSegment;
+import com.android.ddmlib.ClientData.HeapData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Base Panel for heap panels.
+ */
+public abstract class BaseHeapPanel extends TablePanel {
+
+ /** store the processed heap segment, so that we don't recompute Image for nothing */
+ protected byte[] mProcessedHeapData;
+ private Map<Integer, ArrayList<HeapSegmentElement>> mHeapMap;
+
+ /**
+ * Serialize the heap data into an array. The resulting array is available through
+ * <code>getSerializedData()</code>.
+ * @param heapData The heap data to serialize
+ * @return true if the data changed.
+ */
+ protected boolean serializeHeapData(HeapData heapData) {
+ Collection<HeapSegment> heapSegments;
+
+ // Atomically get and clear the heap data.
+ synchronized (heapData) {
+ // get the segments
+ heapSegments = heapData.getHeapSegments();
+
+
+ if (heapSegments != null) {
+ // if they are not null, we never processed them.
+ // Before we process then, we drop them from the HeapData
+ heapData.clearHeapData();
+
+ // process them into a linear byte[]
+ doSerializeHeapData(heapSegments);
+ heapData.setProcessedHeapData(mProcessedHeapData);
+ heapData.setProcessedHeapMap(mHeapMap);
+
+ } else {
+ // the heap segments are null. Let see if the heapData contains a
+ // list that is already processed.
+
+ byte[] pixData = heapData.getProcessedHeapData();
+
+ // and compare it to the one we currently have in the panel.
+ if (pixData == mProcessedHeapData) {
+ // looks like its the same
+ return false;
+ } else {
+ mProcessedHeapData = pixData;
+ }
+
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+ heapData.getProcessedHeapMap();
+ mHeapMap = heapMap;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the serialized heap data
+ */
+ protected byte[] getSerializedData() {
+ return mProcessedHeapData;
+ }
+
+ /**
+ * Processes and serialize the heapData.
+ * <p/>
+ * The resulting serialized array is {@link #mProcessedHeapData}.
+ * <p/>
+ * the resulting map is {@link #mHeapMap}.
+ * @param heapData the collection of {@link HeapSegment} that forms the heap data.
+ */
+ private void doSerializeHeapData(Collection<HeapSegment> heapData) {
+ mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>();
+
+ Iterator<HeapSegment> iterator;
+ ByteArrayOutputStream out;
+
+ out = new ByteArrayOutputStream(4 * 1024);
+
+ iterator = heapData.iterator();
+ while (iterator.hasNext()) {
+ HeapSegment hs = iterator.next();
+
+ HeapSegmentElement e = null;
+ while (true) {
+ int v;
+
+ e = hs.getNextElement(null);
+ if (e == null) {
+ break;
+ }
+
+ if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) {
+ v = 1;
+ } else {
+ v = e.getKind() + 2;
+ }
+
+ // put the element in the map
+ ArrayList<HeapSegmentElement> elementList = mHeapMap.get(v);
+ if (elementList == null) {
+ elementList = new ArrayList<HeapSegmentElement>();
+ mHeapMap.put(v, elementList);
+ }
+ elementList.add(e);
+
+
+ int len = e.getLength() / 8;
+ while (len > 0) {
+ out.write(v);
+ --len;
+ }
+ }
+ }
+ mProcessedHeapData = out.toByteArray();
+
+ // sort the segment element in the heap info.
+ Collection<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values();
+ for (ArrayList<HeapSegmentElement> elementList : elementLists) {
+ Collections.sort(elementList);
+ }
+ }
+
+ /**
+ * Creates a linear image of the heap data.
+ * @param pixData
+ * @param h
+ * @param palette
+ * @return
+ */
+ protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) {
+ int w = pixData.length / h;
+ if (pixData.length % h != 0) {
+ w++;
+ }
+
+ // Create the heap image.
+ ImageData id = new ImageData(w, h, 8, palette);
+
+ int x = 0;
+ int y = 0;
+ for (byte b : pixData) {
+ if (b >= 0) {
+ id.setPixel(x, y, b);
+ }
+
+ y++;
+ if (y >= h) {
+ y = 0;
+ x++;
+ }
+ }
+
+ return id;
+ }
+
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java
new file mode 100644
index 0000000..a711933
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+public abstract class ClientDisplayPanel extends SelectionDependentPanel
+ implements IClientChangeListener {
+
+ @Override
+ protected void postCreation() {
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ public void dispose() {
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java
new file mode 100644
index 0000000..db3642b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+/**
+ * Preference entry point for ddmuilib. Allows the lib to access a preference
+ * store (org.eclipse.jface.preference.IPreferenceStore) defined by the
+ * application that includes the lib.
+ */
+public final class DdmUiPreferences {
+
+ public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds
+
+ private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL;
+
+ private static IPreferenceStore mStore;
+
+ private static String sSymbolLocation =""; //$NON-NLS-1$
+ private static String sAddr2LineLocation =""; //$NON-NLS-1$
+ private static String sTraceviewLocation =""; //$NON-NLS-1$
+
+ public static void setStore(IPreferenceStore store) {
+ mStore = store;
+ }
+
+ public static IPreferenceStore getStore() {
+ return mStore;
+ }
+
+ public static int getThreadRefreshInterval() {
+ return sThreadRefreshInterval;
+ }
+
+ public static void setThreadRefreshInterval(int port) {
+ sThreadRefreshInterval = port;
+ }
+
+ public static String getSymbolDirectory() {
+ return sSymbolLocation;
+ }
+
+ public static void setSymbolsLocation(String location) {
+ sSymbolLocation = location;
+ }
+
+ public static String getAddr2Line() {
+ return sAddr2LineLocation;
+ }
+
+ public static void setAddr2LineLocation(String location) {
+ sAddr2LineLocation = location;
+ }
+
+ public static String getTraceview() {
+ return sTraceviewLocation;
+ }
+
+ public static void setTraceviewLocation(String location) {
+ sTraceviewLocation = location;
+ }
+
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
new file mode 100644
index 0000000..a24b8a0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.DebuggerStatus;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IDevice.DeviceState;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A display of both the devices and their clients.
+ */
+public final class DevicePanel extends Panel implements IDebugBridgeChangeListener,
+ IDeviceChangeListener, IClientChangeListener {
+
+ private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$
+
+ private final static int DEVICE_COL_SERIAL = 0;
+ private final static int DEVICE_COL_STATE = 1;
+ // col 2, 3 not used.
+ private final static int DEVICE_COL_BUILD = 4;
+
+ private final static int CLIENT_COL_NAME = 0;
+ private final static int CLIENT_COL_PID = 1;
+ private final static int CLIENT_COL_THREAD = 2;
+ private final static int CLIENT_COL_HEAP = 3;
+ private final static int CLIENT_COL_PORT = 4;
+
+ public final static int ICON_WIDTH = 16;
+ public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$
+ public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$
+ public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$
+ public final static String ICON_GC = "gc.png"; //$NON-NLS-1$
+ public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$
+ public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$
+ public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$
+
+ private IDevice mCurrentDevice;
+ private Client mCurrentClient;
+
+ private Tree mTree;
+ private TreeViewer mTreeViewer;
+
+ private Image mDeviceImage;
+ private Image mEmulatorImage;
+
+ private Image mThreadImage;
+ private Image mHeapImage;
+ private Image mWaitingImage;
+ private Image mDebuggerImage;
+ private Image mDebugErrorImage;
+
+ private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>();
+
+ private final ArrayList<IDevice> mDevicesToExpand = new ArrayList<IDevice>();
+
+ private boolean mAdvancedPortSupport;
+
+ /**
+ * A Content provider for the {@link TreeViewer}.
+ * <p/>
+ * The input is a {@link AndroidDebugBridge}. First level elements are {@link IDevice} objects,
+ * and second level elements are {@link Client} object.
+ */
+ private class ContentProvider implements ITreeContentProvider {
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof IDevice) {
+ return ((IDevice)parentElement).getClients();
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof Client) {
+ return ((Client)element).getDevice();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof IDevice) {
+ return ((IDevice)element).hasClients();
+ }
+
+ // Clients never have children.
+ return false;
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof AndroidDebugBridge) {
+ return ((AndroidDebugBridge)inputElement).getDevices();
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides
+ * labels and images for {@link IDevice} and {@link Client} objects.
+ */
+ private class LabelProvider implements ITableLabelProvider {
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == DEVICE_COL_SERIAL && element instanceof IDevice) {
+ IDevice device = (IDevice)element;
+ if (device.isEmulator()) {
+ return mEmulatorImage;
+ }
+
+ return mDeviceImage;
+ } else if (element instanceof Client) {
+ Client client = (Client)element;
+ ClientData cd = client.getClientData();
+
+ switch (columnIndex) {
+ case CLIENT_COL_NAME:
+ switch (cd.getDebuggerConnectionStatus()) {
+ case DEFAULT:
+ return null;
+ case WAITING:
+ return mWaitingImage;
+ case ATTACHED:
+ return mDebuggerImage;
+ case ERROR:
+ return mDebugErrorImage;
+ }
+ return null;
+ case CLIENT_COL_THREAD:
+ if (client.isThreadUpdateEnabled()) {
+ return mThreadImage;
+ }
+ return null;
+ case CLIENT_COL_HEAP:
+ if (client.isHeapUpdateEnabled()) {
+ return mHeapImage;
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof IDevice) {
+ IDevice device = (IDevice)element;
+ switch (columnIndex) {
+ case DEVICE_COL_SERIAL:
+ return device.getName();
+ case DEVICE_COL_STATE:
+ return getStateString(device);
+ case DEVICE_COL_BUILD: {
+ String version = device.getProperty(IDevice.PROP_BUILD_VERSION);
+ if (version != null) {
+ String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
+ if (device.isEmulator()) {
+ String avdName = device.getAvdName();
+ if (avdName == null) {
+ avdName = "?"; // the device is probably not online yet, so
+ // we don't know its AVD name just yet.
+ }
+ if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+ return String.format("%1$s [%2$s, debug]", avdName,
+ version);
+ } else {
+ return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$
+ }
+ } else {
+ if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+ return String.format("%1$s, debug", version);
+ } else {
+ return String.format("%1$s", version); //$NON-NLS-1$
+ }
+ }
+ } else {
+ return "unknown";
+ }
+ }
+ }
+ } else if (element instanceof Client) {
+ Client client = (Client)element;
+ ClientData cd = client.getClientData();
+
+ switch (columnIndex) {
+ case CLIENT_COL_NAME:
+ String name = cd.getClientDescription();
+ if (name != null) {
+ if (cd.isValidUserId() && cd.getUserId() != 0) {
+ return String.format(Locale.US, "%s (%d)", name, cd.getUserId());
+ } else {
+ return name;
+ }
+ }
+ return "?";
+ case CLIENT_COL_PID:
+ return Integer.toString(cd.getPid());
+ case CLIENT_COL_PORT:
+ if (mAdvancedPortSupport) {
+ int port = client.getDebuggerListenPort();
+ String portString = "?";
+ if (port != 0) {
+ portString = Integer.toString(port);
+ }
+ if (client.isSelectedClient()) {
+ return String.format("%1$s / %2$d", portString, //$NON-NLS-1$
+ DdmPreferences.getSelectedDebugPort());
+ }
+
+ return portString;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deals
+ * with {@link IDevice} and {@link Client} selection changes coming from the ui.
+ */
+ public interface IUiSelectionListener {
+ /**
+ * Sent when a new {@link IDevice} and {@link Client} are selected.
+ * @param selectedDevice the selected device. If null, no devices are selected.
+ * @param selectedClient The selected client. If null, no clients are selected.
+ */
+ public void selectionChanged(IDevice selectedDevice, Client selectedClient);
+ }
+
+ /**
+ * Creates the {@link DevicePanel} object.
+ * @param loader
+ * @param advancedPortSupport if true the device panel will add support for selected client port
+ * and display the ports in the ui.
+ */
+ public DevicePanel(boolean advancedPortSupport) {
+ mAdvancedPortSupport = advancedPortSupport;
+ }
+
+ public void addSelectionListener(IUiSelectionListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeSelectionListener(IUiSelectionListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ loadImages(parent.getDisplay());
+
+ parent.setLayout(new FillLayout());
+
+ // create the tree and its column
+ mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION);
+ mTree.setHeaderVisible(true);
+ mTree.setLinesVisible(true);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+ "com.android.home", //$NON-NLS-1$
+ PREFS_COL_NAME_SERIAL, store);
+ TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+ "Offline", //$NON-NLS-1$
+ PREFS_COL_PID_STATE, store);
+
+ TreeColumn col = new TreeColumn(mTree, SWT.NONE);
+ col.setWidth(ICON_WIDTH + 8);
+ col.setResizable(false);
+ col = new TreeColumn(mTree, SWT.NONE);
+ col.setWidth(ICON_WIDTH + 8);
+ col.setResizable(false);
+
+ TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+ "9999-9999", //$NON-NLS-1$
+ PREFS_COL_PORT_BUILD, store);
+
+ // create the tree viewer
+ mTreeViewer = new TreeViewer(mTree);
+
+ // make the device auto expanded.
+ mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+ // set up the content and label providers.
+ mTreeViewer.setContentProvider(new ContentProvider());
+ mTreeViewer.setLabelProvider(new LabelProvider());
+
+ mTree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ notifyListeners();
+ }
+ });
+
+ return mTree;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTree.setFocus();
+ }
+
+ @Override
+ protected void postCreation() {
+ // ask for notification of changes in AndroidDebugBridge (a new one is created when
+ // adb is restarted from a different location), IDevice and Client objects.
+ AndroidDebugBridge.addDebugBridgeChangeListener(this);
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ public void dispose() {
+ AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+
+ /**
+ * Returns the selected {@link Client}. May be null.
+ */
+ public Client getSelectedClient() {
+ return mCurrentClient;
+ }
+
+ /**
+ * Returns the selected {@link IDevice}. If a {@link Client} is selected, it returns the
+ * IDevice object containing the client.
+ */
+ public IDevice getSelectedDevice() {
+ return mCurrentDevice;
+ }
+
+ /**
+ * Kills the selected {@link Client} by sending its VM a halt command.
+ */
+ public void killSelectedClient() {
+ if (mCurrentClient != null) {
+ Client client = mCurrentClient;
+
+ // reset the selection to the device.
+ TreePath treePath = new TreePath(new Object[] { mCurrentDevice });
+ TreeSelection treeSelection = new TreeSelection(treePath);
+ mTreeViewer.setSelection(treeSelection);
+
+ client.kill();
+ }
+ }
+
+ /**
+ * Forces a GC on the selected {@link Client}.
+ */
+ public void forceGcOnSelectedClient() {
+ if (mCurrentClient != null) {
+ mCurrentClient.executeGarbageCollector();
+ }
+ }
+
+ public void dumpHprof() {
+ if (mCurrentClient != null) {
+ mCurrentClient.dumpHprof();
+ }
+ }
+
+ public void toggleMethodProfiling() {
+ if (mCurrentClient != null) {
+ mCurrentClient.toggleMethodProfiling();
+ }
+ }
+
+ public void setEnabledHeapOnSelectedClient(boolean enable) {
+ if (mCurrentClient != null) {
+ mCurrentClient.setHeapUpdateEnabled(enable);
+ }
+ }
+
+ public void setEnabledThreadOnSelectedClient(boolean enable) {
+ if (mCurrentClient != null) {
+ mCurrentClient.setThreadUpdateEnabled(enable);
+ }
+ }
+
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is started.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ *
+ * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge)
+ */
+ @Override
+ public void bridgeChanged(final AndroidDebugBridge bridge) {
+ if (mTree.isDisposed() == false) {
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // set up the data source.
+ mTreeViewer.setInput(bridge);
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ // all current devices are obsolete
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.clear();
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(IDevice)
+ */
+ @Override
+ public void deviceConnected(IDevice device) {
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // refresh all
+ mTreeViewer.refresh();
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+
+ // if it doesn't have clients yet, it'll need to be manually expanded when it gets them.
+ if (device.hasClients() == false) {
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.add(device);
+ }
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(IDevice)
+ */
+ @Override
+ public void deviceDisconnected(IDevice device) {
+ deviceConnected(device);
+
+ // just in case, we remove it from the list of devices to expand.
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.remove(device);
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask indicating what changed.
+ *
+ * @see IDeviceChangeListener#deviceChanged(IDevice)
+ */
+ @Override
+ public void deviceChanged(final IDevice device, int changeMask) {
+ boolean expand = false;
+ synchronized (mDevicesToExpand) {
+ int index = mDevicesToExpand.indexOf(device);
+ if (device.hasClients() && index != -1) {
+ mDevicesToExpand.remove(index);
+ expand = true;
+ }
+ }
+
+ final boolean finalExpand = expand;
+
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // look if the current device is selected. This is done in case the current
+ // client of this particular device was killed. In this case, we'll need to
+ // manually reselect the device.
+
+ IDevice selectedDevice = getSelectedDevice();
+
+ // refresh the device
+ mTreeViewer.refresh(device);
+
+ // if the selected device was the changed device and the new selection is
+ // empty, we reselect the device.
+ if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) {
+ mTreeViewer.setSelection(new TreeSelection(new TreePath(
+ new Object[] { device })));
+ }
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+
+ if (finalExpand) {
+ mTreeViewer.setExpandedState(device, true);
+ }
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO},
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, final int changeMask) {
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // refresh the client
+ mTreeViewer.refresh(client);
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) ==
+ Client.CHANGE_DEBUGGER_STATUS &&
+ client.getClientData().getDebuggerConnectionStatus() ==
+ DebuggerStatus.WAITING) {
+ // make sure the device is expanded. Normally the setSelection below
+ // will auto expand, but the children of device may not already exist
+ // at this time. Forcing an expand will make the TreeViewer create them.
+ IDevice device = client.getDevice();
+ if (mTreeViewer.getExpandedState(device) == false) {
+ mTreeViewer.setExpandedState(device, true);
+ }
+
+ // create and set the selection
+ TreePath treePath = new TreePath(new Object[] { device, client});
+ TreeSelection treeSelection = new TreeSelection(treePath);
+ mTreeViewer.setSelection(treeSelection);
+
+ if (mAdvancedPortSupport) {
+ client.setAsSelectedClient();
+ }
+
+ // notify the listener of a possible selection change.
+ notifyListeners(device, client);
+ }
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ private void loadImages(Display display) {
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+
+ if (mDeviceImage == null) {
+ mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mEmulatorImage == null) {
+ mEmulatorImage = loader.loadImage(display,
+ "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mThreadImage == null) {
+ mThreadImage = loader.loadImage(display, ICON_THREAD,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_YELLOW));
+ }
+ if (mHeapImage == null) {
+ mHeapImage = loader.loadImage(display, ICON_HEAP,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mWaitingImage == null) {
+ mWaitingImage = loader.loadImage(display,
+ "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mDebuggerImage == null) {
+ mDebuggerImage = loader.loadImage(display,
+ "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_GREEN));
+ }
+ if (mDebugErrorImage == null) {
+ mDebugErrorImage = loader.loadImage(display,
+ "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ }
+
+ /**
+ * Returns a display string representing the state of the device.
+ * @param d the device
+ */
+ private static String getStateString(IDevice d) {
+ DeviceState deviceState = d.getState();
+ if (deviceState == DeviceState.ONLINE) {
+ return "Online";
+ } else if (deviceState == DeviceState.OFFLINE) {
+ return "Offline";
+ } else if (deviceState == DeviceState.BOOTLOADER) {
+ return "Bootloader";
+ }
+
+ return "??";
+ }
+
+ /**
+ * Executes the {@link Runnable} in the UI thread.
+ * @param runnable the runnable to execute.
+ */
+ private void exec(Runnable runnable) {
+ try {
+ Display display = mTree.getDisplay();
+ display.asyncExec(runnable);
+ } catch (SWTException e) {
+ // tree is disposed, we need to do something. lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+ }
+
+ private void notifyListeners() {
+ // get the selection
+ TreeItem[] items = mTree.getSelection();
+
+ Client client = null;
+ IDevice device = null;
+
+ if (items.length == 1) {
+ Object object = items[0].getData();
+ if (object instanceof Client) {
+ client = (Client)object;
+ device = client.getDevice();
+ } else if (object instanceof IDevice) {
+ device = (IDevice)object;
+ }
+ }
+
+ notifyListeners(device, client);
+ }
+
+ private void notifyListeners(IDevice selectedDevice, Client selectedClient) {
+ if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) {
+ mCurrentDevice = selectedDevice;
+ mCurrentClient = selectedClient;
+
+ for (IUiSelectionListener listener : mListeners) {
+ // notify the listener with a try/catch-all to make sure this thread won't die
+ // because of an uncaught exception before all the listeners were notified.
+ try {
+ listener.selectionChanged(selectedDevice, selectedClient);
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java
new file mode 100644
index 0000000..82aed98
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java
@@ -0,0 +1,1463 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.EmulatorConsole;
+import com.android.ddmlib.EmulatorConsole.GsmMode;
+import com.android.ddmlib.EmulatorConsole.GsmStatus;
+import com.android.ddmlib.EmulatorConsole.NetworkStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmuilib.location.CoordinateControls;
+import com.android.ddmuilib.location.GpxParser;
+import com.android.ddmuilib.location.GpxParser.Track;
+import com.android.ddmuilib.location.KmlParser;
+import com.android.ddmuilib.location.TrackContentProvider;
+import com.android.ddmuilib.location.TrackLabelProvider;
+import com.android.ddmuilib.location.TrackPoint;
+import com.android.ddmuilib.location.WayPoint;
+import com.android.ddmuilib.location.WayPointContentProvider;
+import com.android.ddmuilib.location.WayPointLabelProvider;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Panel to control the emulator using EmulatorConsole objects.
+ */
+public class EmulatorControlPanel extends SelectionDependentPanel {
+
+ // default location: Patio outside Charlie's
+ private final static double DEFAULT_LONGITUDE = -122.084095;
+ private final static double DEFAULT_LATITUDE = 37.422006;
+
+ private final static String SPEED_FORMAT = "Speed: %1$dX";
+
+
+ /**
+ * Map between the display gsm mode and the internal tag used by the display.
+ */
+ private final static String[][] GSM_MODES = new String[][] {
+ { "unregistered", GsmMode.UNREGISTERED.getTag() },
+ { "home", GsmMode.HOME.getTag() },
+ { "roaming", GsmMode.ROAMING.getTag() },
+ { "searching", GsmMode.SEARCHING.getTag() },
+ { "denied", GsmMode.DENIED.getTag() },
+ };
+
+ private final static String[] NETWORK_SPEEDS = new String[] {
+ "Full",
+ "GSM",
+ "HSCSD",
+ "GPRS",
+ "EDGE",
+ "UMTS",
+ "HSDPA",
+ };
+
+ private final static String[] NETWORK_LATENCIES = new String[] {
+ "None",
+ "GPRS",
+ "EDGE",
+ "UMTS",
+ };
+
+ private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 };
+
+ private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$
+
+ private EmulatorConsole mEmulatorConsole;
+
+ private Composite mParent;
+
+ private Label mVoiceLabel;
+ private Combo mVoiceMode;
+ private Label mDataLabel;
+ private Combo mDataMode;
+ private Label mSpeedLabel;
+ private Combo mNetworkSpeed;
+ private Label mLatencyLabel;
+ private Combo mNetworkLatency;
+
+ private Label mNumberLabel;
+ private Text mPhoneNumber;
+
+ private Button mVoiceButton;
+ private Button mSmsButton;
+
+ private Label mMessageLabel;
+ private Text mSmsMessage;
+
+ private Button mCallButton;
+ private Button mCancelButton;
+
+ private TabFolder mLocationFolders;
+
+ private Button mDecimalButton;
+ private Button mSexagesimalButton;
+ private CoordinateControls mLongitudeControls;
+ private CoordinateControls mLatitudeControls;
+ private Button mGpxUploadButton;
+ private Table mGpxWayPointTable;
+ private Table mGpxTrackTable;
+ private Button mKmlUploadButton;
+ private Table mKmlWayPointTable;
+
+ private Button mPlayGpxButton;
+ private Button mGpxBackwardButton;
+ private Button mGpxForwardButton;
+ private Button mGpxSpeedButton;
+ private Button mPlayKmlButton;
+ private Button mKmlBackwardButton;
+ private Button mKmlForwardButton;
+ private Button mKmlSpeedButton;
+
+ private Image mPlayImage;
+ private Image mPauseImage;
+
+ private Thread mPlayingThread;
+ private boolean mPlayingTrack;
+ private int mPlayDirection = 1;
+ private int mSpeed;
+ private int mSpeedIndex;
+
+ private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Button b = (Button)e.getSource();
+ if (b.getSelection() == false) {
+ // basically the button was unselected, which we don't allow.
+ // so we reselect it.
+ b.setSelection(true);
+ return;
+ }
+
+ // now handle selection change.
+ if (b == mGpxForwardButton || b == mKmlForwardButton) {
+ mGpxBackwardButton.setSelection(false);
+ mGpxForwardButton.setSelection(true);
+ mKmlBackwardButton.setSelection(false);
+ mKmlForwardButton.setSelection(true);
+ mPlayDirection = 1;
+
+ } else {
+ mGpxBackwardButton.setSelection(true);
+ mGpxForwardButton.setSelection(false);
+ mKmlBackwardButton.setSelection(true);
+ mKmlForwardButton.setSelection(false);
+ mPlayDirection = -1;
+ }
+ }
+ };
+
+ private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mGpxPlayControls.pack();
+ mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mKmlPlayControls.pack();
+
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ };
+ private Composite mKmlPlayControls;
+ private Composite mGpxPlayControls;
+
+
+ public EmulatorControlPanel() {
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ handleNewDevice(getCurrentDevice());
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+
+ final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL);
+ scollingParent.setExpandVertical(true);
+ scollingParent.setExpandHorizontal(true);
+ scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ final Composite top = new Composite(scollingParent, SWT.NONE);
+ scollingParent.setContent(top);
+ top.setLayout(new GridLayout(1, false));
+
+ // set the resize for the scrolling to work (why isn't that done automatically?!?)
+ scollingParent.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = scollingParent.getClientArea();
+ scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+ }
+ });
+
+ createRadioControls(top);
+
+ createCallControls(top);
+
+ createLocationControls(top);
+
+ doEnable(false);
+
+ top.layout();
+ Rectangle r = scollingParent.getClientArea();
+ scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+
+ return scollingParent;
+ }
+
+ /**
+ * Create Radio (on/off/roaming, for voice/data) controls.
+ * @param top
+ */
+ private void createRadioControls(final Composite top) {
+ Group g1 = new Group(top, SWT.NONE);
+ g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ g1.setLayout(new GridLayout(2, false));
+ g1.setText("Telephony Status");
+
+ // the inside of the group is 2 composite so that all the column of the controls (mainly
+ // combos) have the same width, while not taking the whole screen width
+ Composite insideGroup = new Composite(g1, SWT.NONE);
+ GridLayout gl = new GridLayout(4, false);
+ gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+ insideGroup.setLayout(gl);
+
+ mVoiceLabel = new Label(insideGroup, SWT.NONE);
+ mVoiceLabel.setText("Voice:");
+ mVoiceLabel.setAlignment(SWT.RIGHT);
+
+ mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY);
+ mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String[] mode : GSM_MODES) {
+ mVoiceMode.add(mode[0]);
+ }
+ mVoiceMode.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setVoiceMode(mVoiceMode.getSelectionIndex());
+ }
+ });
+
+ mSpeedLabel = new Label(insideGroup, SWT.NONE);
+ mSpeedLabel.setText("Speed:");
+ mSpeedLabel.setAlignment(SWT.RIGHT);
+
+ mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY);
+ mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String mode : NETWORK_SPEEDS) {
+ mNetworkSpeed.add(mode);
+ }
+ mNetworkSpeed.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setNetworkSpeed(mNetworkSpeed.getSelectionIndex());
+ }
+ });
+
+ mDataLabel = new Label(insideGroup, SWT.NONE);
+ mDataLabel.setText("Data:");
+ mDataLabel.setAlignment(SWT.RIGHT);
+
+ mDataMode = new Combo(insideGroup, SWT.READ_ONLY);
+ mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String[] mode : GSM_MODES) {
+ mDataMode.add(mode[0]);
+ }
+ mDataMode.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setDataMode(mDataMode.getSelectionIndex());
+ }
+ });
+
+ mLatencyLabel = new Label(insideGroup, SWT.NONE);
+ mLatencyLabel.setText("Latency:");
+ mLatencyLabel.setAlignment(SWT.RIGHT);
+
+ mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY);
+ mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String mode : NETWORK_LATENCIES) {
+ mNetworkLatency.add(mode);
+ }
+ mNetworkLatency.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setNetworkLatency(mNetworkLatency.getSelectionIndex());
+ }
+ });
+
+ // now an empty label to take the rest of the width of the group
+ Label l = new Label(g1, SWT.NONE);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ }
+
+ /**
+ * Create Voice/SMS call/hang up controls
+ * @param top
+ */
+ private void createCallControls(final Composite top) {
+ GridLayout gl;
+ Group g2 = new Group(top, SWT.NONE);
+ g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ g2.setLayout(new GridLayout(1, false));
+ g2.setText("Telephony Actions");
+
+ // horizontal composite for label + text field
+ Composite phoneComp = new Composite(g2, SWT.NONE);
+ phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH));
+ gl = new GridLayout(2, false);
+ gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+ phoneComp.setLayout(gl);
+
+ mNumberLabel = new Label(phoneComp, SWT.NONE);
+ mNumberLabel.setText("Incoming number:");
+
+ mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+ mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mPhoneNumber.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ // Reenable the widgets based on the content of the text.
+ // doEnable checks the validity of the phone number to enable/disable some
+ // widgets.
+ // Looks like we're getting a callback at creation time, so we can't
+ // suppose that we are enabled when the text is modified...
+ doEnable(mEmulatorConsole != null);
+ }
+ });
+
+ mVoiceButton = new Button(phoneComp, SWT.RADIO);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 2;
+ mVoiceButton.setText("Voice");
+ mVoiceButton.setLayoutData(gd);
+ mVoiceButton.setEnabled(false);
+ mVoiceButton.setSelection(true);
+ mVoiceButton.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ doEnable(true);
+
+ if (mVoiceButton.getSelection()) {
+ mCallButton.setText("Call");
+ } else {
+ mCallButton.setText("Send");
+ }
+ }
+ });
+
+ mSmsButton = new Button(phoneComp, SWT.RADIO);
+ mSmsButton.setText("SMS");
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ mSmsButton.setLayoutData(gd);
+ mSmsButton.setEnabled(false);
+ // Since there are only 2 radio buttons, we can put a listener on only one (they
+ // are both called on select and unselect event.
+
+ mMessageLabel = new Label(phoneComp, SWT.NONE);
+ gd = new GridData();
+ gd.verticalAlignment = SWT.TOP;
+ mMessageLabel.setLayoutData(gd);
+ mMessageLabel.setText("Message:");
+ mMessageLabel.setEnabled(false);
+
+ mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
+ mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 70;
+ mSmsMessage.setEnabled(false);
+
+ // composite to put the 2 buttons horizontally
+ Composite g2ButtonComp = new Composite(g2, SWT.NONE);
+ g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ gl = new GridLayout(2, false);
+ gl.marginWidth = gl.marginHeight = 0;
+ g2ButtonComp.setLayout(gl);
+
+ // now a button below the phone number
+ mCallButton = new Button(g2ButtonComp, SWT.PUSH);
+ mCallButton.setText("Call");
+ mCallButton.setEnabled(false);
+ mCallButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ if (mVoiceButton.getSelection()) {
+ processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim()));
+ } else {
+ // we need to encode the message. We need to replace the carriage return
+ // character by the 2 character string \n.
+ // Because of this the \ character needs to be escaped as well.
+ // ReplaceAll() expects regexp so \ char are escaped twice.
+ String message = mSmsMessage.getText();
+ message = message.replaceAll("\\\\", //$NON-NLS-1$
+ "\\\\\\\\"); //$NON-NLS-1$
+
+ // While the normal line delimiter is returned by Text.getLineDelimiter()
+ // it seems copy pasting text coming from somewhere else could have another
+ // delimited. For this reason, we'll replace is several steps
+
+ // replace the dual CR-LF
+ message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // replace remaining stand alone \n
+ message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // replace remaining stand alone \r
+ message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(),
+ message));
+ }
+ }
+ }
+ });
+
+ mCancelButton = new Button(g2ButtonComp, SWT.PUSH);
+ mCancelButton.setText("Hang Up");
+ mCancelButton.setEnabled(false);
+ mCancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ if (mVoiceButton.getSelection()) {
+ processCommandResult(mEmulatorConsole.cancelCall(
+ mPhoneNumber.getText().trim()));
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Create Location controls.
+ * @param top
+ */
+ private void createLocationControls(final Composite top) {
+ Label l = new Label(top, SWT.NONE);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ l.setText("Location Controls");
+
+ mLocationFolders = new TabFolder(top, SWT.NONE);
+ mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ TabItem item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("Manual");
+ item.setControl(manualLocationComp);
+
+ createManualLocationControl(manualLocationComp);
+
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+ mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$
+ mPauseImage = loader.loadImage("pause.png", mParent.getDisplay()); //$NON-NLS-1$
+
+ Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("GPX");
+ item.setControl(gpxLocationComp);
+
+ createGpxLocationControl(gpxLocationComp);
+
+ Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ kmlLocationComp.setLayout(new FillLayout());
+ item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("KML");
+ item.setControl(kmlLocationComp);
+
+ createKmlLocationControl(kmlLocationComp);
+ }
+
+ private void createManualLocationControl(Composite manualLocationComp) {
+ final StackLayout sl;
+ GridLayout gl;
+ Label label;
+
+ manualLocationComp.setLayout(new GridLayout(1, false));
+ mDecimalButton = new Button(manualLocationComp, SWT.RADIO);
+ mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDecimalButton.setText("Decimal");
+ mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO);
+ mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mSexagesimalButton.setText("Sexagesimal");
+
+ // composite to hold and switching between the 2 modes.
+ final Composite content = new Composite(manualLocationComp, SWT.NONE);
+ content.setLayout(sl = new StackLayout());
+
+ // decimal display
+ final Composite decimalContent = new Composite(content, SWT.NONE);
+ decimalContent.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mLongitudeControls = new CoordinateControls();
+ mLatitudeControls = new CoordinateControls();
+
+ label = new Label(decimalContent, SWT.NONE);
+ label.setText("Longitude");
+
+ mLongitudeControls.createDecimalText(decimalContent);
+
+ label = new Label(decimalContent, SWT.NONE);
+ label.setText("Latitude");
+
+ mLatitudeControls.createDecimalText(decimalContent);
+
+ // sexagesimal content
+ final Composite sexagesimalContent = new Composite(content, SWT.NONE);
+ sexagesimalContent.setLayout(gl = new GridLayout(7, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("Longitude");
+
+ mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\u00B0"); // degree character
+
+ mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("'");
+
+ mLongitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\"");
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("Latitude");
+
+ mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\u00B0");
+
+ mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("'");
+
+ mLatitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\"");
+
+ // set the default display to decimal
+ sl.topControl = decimalContent;
+ mDecimalButton.setSelection(true);
+
+ mDecimalButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mDecimalButton.getSelection()) {
+ sl.topControl = decimalContent;
+ } else {
+ sl.topControl = sexagesimalContent;
+ }
+ content.layout();
+ }
+ });
+
+ Button sendButton = new Button(manualLocationComp, SWT.PUSH);
+ sendButton.setText("Send");
+ sendButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0));
+ }
+ }
+ });
+
+ mLongitudeControls.setValue(DEFAULT_LONGITUDE);
+ mLatitudeControls.setValue(DEFAULT_LATITUDE);
+ }
+
+ private void createGpxLocationControl(Composite gpxLocationComp) {
+ GridData gd;
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ gpxLocationComp.setLayout(new GridLayout(1, false));
+
+ mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH);
+ mGpxUploadButton.setText("Load GPX...");
+
+ // Table for way point
+ mGpxWayPointTable = new Table(gpxLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 100;
+ mGpxWayPointTable.setHeaderVisible(true);
+ mGpxWayPointTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT,
+ "Some Name",
+ PREFS_WAYPOINT_COL_NAME, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LONGITUDE, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LATITUDE, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT,
+ "99999.9",
+ PREFS_WAYPOINT_COL_ELEVATION, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT,
+ "Some Description",
+ PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+ final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable);
+ gpxWayPointViewer.setContentProvider(new WayPointContentProvider());
+ gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+ gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ wayPoint.getLongitude(), wayPoint.getLatitude(),
+ wayPoint.getElevation()));
+ }
+ }
+ }
+ }
+ });
+
+ // table for tracks.
+ mGpxTrackTable = new Table(gpxLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 100;
+ mGpxTrackTable.setHeaderVisible(true);
+ mGpxTrackTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT,
+ "Some very long name",
+ PREFS_TRACK_COL_NAME, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT,
+ "9999",
+ PREFS_TRACK_COL_COUNT, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT,
+ "999-99-99T99:99:99Z",
+ PREFS_TRACK_COL_FIRST, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT,
+ "999-99-99T99:99:99Z",
+ PREFS_TRACK_COL_LAST, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT,
+ "-199.999999",
+ PREFS_TRACK_COL_COMMENT, store);
+
+ final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable);
+ gpxTrackViewer.setContentProvider(new TrackContentProvider());
+ gpxTrackViewer.setLabelProvider(new TrackLabelProvider());
+
+ gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof Track) {
+ Track track = (Track)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ TrackPoint[] points = track.getPoints();
+ processCommandResult(mEmulatorConsole.sendLocation(
+ points[0].getLongitude(), points[0].getLatitude(),
+ points[0].getElevation()));
+ }
+
+ mPlayGpxButton.setEnabled(true);
+ mGpxBackwardButton.setEnabled(true);
+ mGpxForwardButton.setEnabled(true);
+ mGpxSpeedButton.setEnabled(true);
+
+ return;
+ }
+ }
+
+ mPlayGpxButton.setEnabled(false);
+ mGpxBackwardButton.setEnabled(false);
+ mGpxForwardButton.setEnabled(false);
+ mGpxSpeedButton.setEnabled(false);
+ }
+ });
+
+ mGpxUploadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load GPX File");
+ fileDialog.setFilterExtensions(new String[] { "*.gpx" } );
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ GpxParser parser = new GpxParser(fileName);
+ if (parser.parse()) {
+ gpxWayPointViewer.setInput(parser.getWayPoints());
+ gpxTrackViewer.setInput(parser.getTracks());
+ }
+ }
+ }
+ });
+
+ mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE);
+ GridLayout gl;
+ mGpxPlayControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayGpxButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPlayingTrack == false) {
+ ISelection selection = gpxTrackViewer.getSelection();
+ if (selection.isEmpty() == false && selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof Track) {
+ Track track = (Track)selectedObject;
+ playTrack(track);
+ }
+ }
+ } else {
+ // if we're playing, then we pause
+ mPlayingTrack = false;
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ }
+ });
+
+ Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+ mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
+ mGpxBackwardButton.setSelection(false);
+ mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
+ mGpxForwardButton.setSelection(true);
+ mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+ mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+
+ mSpeedIndex = 0;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+ mPlayGpxButton.setEnabled(false);
+ mGpxBackwardButton.setEnabled(false);
+ mGpxForwardButton.setEnabled(false);
+ mGpxSpeedButton.setEnabled(false);
+
+ }
+
+ private void createKmlLocationControl(Composite kmlLocationComp) {
+ GridData gd;
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ kmlLocationComp.setLayout(new GridLayout(1, false));
+
+ mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH);
+ mKmlUploadButton.setText("Load KML...");
+
+ // Table for way point
+ mKmlWayPointTable = new Table(kmlLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 200;
+ mKmlWayPointTable.setHeaderVisible(true);
+ mKmlWayPointTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT,
+ "Some Name",
+ PREFS_WAYPOINT_COL_NAME, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LONGITUDE, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LATITUDE, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT,
+ "99999.9",
+ PREFS_WAYPOINT_COL_ELEVATION, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT,
+ "Some Description",
+ PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+ final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable);
+ kmlWayPointViewer.setContentProvider(new WayPointContentProvider());
+ kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+ mKmlUploadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load KML File");
+ fileDialog.setFilterExtensions(new String[] { "*.kml" } );
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ KmlParser parser = new KmlParser(fileName);
+ if (parser.parse()) {
+ kmlWayPointViewer.setInput(parser.getWayPoints());
+
+ mPlayKmlButton.setEnabled(true);
+ mKmlBackwardButton.setEnabled(true);
+ mKmlForwardButton.setEnabled(true);
+ mKmlSpeedButton.setEnabled(true);
+ }
+ }
+ }
+ });
+
+ kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ wayPoint.getLongitude(), wayPoint.getLatitude(),
+ wayPoint.getElevation()));
+ }
+ }
+ }
+ }
+ });
+
+
+
+ mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE);
+ GridLayout gl;
+ mKmlPlayControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+ mPlayKmlButton.setImage(mPlayImage);
+ mPlayKmlButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPlayingTrack == false) {
+ Object input = kmlWayPointViewer.getInput();
+ if (input instanceof WayPoint[]) {
+ playKml((WayPoint[])input);
+ }
+ } else {
+ // if we're playing, then we pause
+ mPlayingTrack = false;
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ }
+ });
+
+ Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+ mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
+ mKmlBackwardButton.setSelection(false);
+ mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
+ mKmlForwardButton.setSelection(true);
+ mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+ mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+
+ mSpeedIndex = 0;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+ mPlayKmlButton.setEnabled(false);
+ mKmlBackwardButton.setEnabled(false);
+ mKmlForwardButton.setEnabled(false);
+ mKmlSpeedButton.setEnabled(false);
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ }
+
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ private synchronized void setDataMode(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setGsmDataMode(
+ GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+ }
+ }
+
+ private synchronized void setVoiceMode(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setGsmVoiceMode(
+ GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+ }
+ }
+
+ private synchronized void setNetworkLatency(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex));
+ }
+ }
+
+ private synchronized void setNetworkSpeed(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex));
+ }
+ }
+
+
+ /**
+ * Callback on device selection change.
+ * @param device the new selected device
+ */
+ public void handleNewDevice(IDevice device) {
+ if (mParent.isDisposed()) {
+ return;
+ }
+ // unlink to previous console.
+ synchronized (this) {
+ mEmulatorConsole = null;
+ }
+
+ try {
+ // get the emulator console for this device
+ // First we need the device itself
+ if (device != null) {
+ GsmStatus gsm = null;
+ NetworkStatus netstatus = null;
+
+ synchronized (this) {
+ mEmulatorConsole = EmulatorConsole.getConsole(device);
+ if (mEmulatorConsole != null) {
+ // get the gsm status
+ gsm = mEmulatorConsole.getGsmStatus();
+ netstatus = mEmulatorConsole.getNetworkStatus();
+
+ if (gsm == null || netstatus == null) {
+ mEmulatorConsole = null;
+ }
+ }
+ }
+
+ if (gsm != null && netstatus != null) {
+ Display d = mParent.getDisplay();
+ if (d.isDisposed() == false) {
+ final GsmStatus f_gsm = gsm;
+ final NetworkStatus f_netstatus = netstatus;
+
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (f_gsm.voice != GsmMode.UNKNOWN) {
+ mVoiceMode.select(getGsmComboIndex(f_gsm.voice));
+ } else {
+ mVoiceMode.clearSelection();
+ }
+ if (f_gsm.data != GsmMode.UNKNOWN) {
+ mDataMode.select(getGsmComboIndex(f_gsm.data));
+ } else {
+ mDataMode.clearSelection();
+ }
+
+ if (f_netstatus.speed != -1) {
+ mNetworkSpeed.select(f_netstatus.speed);
+ } else {
+ mNetworkSpeed.clearSelection();
+ }
+
+ if (f_netstatus.latency != -1) {
+ mNetworkLatency.select(f_netstatus.latency);
+ } else {
+ mNetworkLatency.clearSelection();
+ }
+ }
+ });
+ }
+ }
+ }
+ } finally {
+ // enable/disable the ui
+ boolean enable = false;
+ synchronized (this) {
+ enable = mEmulatorConsole != null;
+ }
+
+ enable(enable);
+ }
+ }
+
+ /**
+ * Enable or disable the ui. Can be called from non ui threads.
+ * @param enabled
+ */
+ private void enable(final boolean enabled) {
+ try {
+ Display d = mParent.getDisplay();
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mParent.isDisposed() == false) {
+ doEnable(enabled);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // disposed. do nothing
+ }
+ }
+
+ private boolean isValidPhoneNumber() {
+ String number = mPhoneNumber.getText().trim();
+
+ return number.matches(RE_PHONE_NUMBER);
+ }
+
+ /**
+ * Enable or disable the ui. Cannot be called from non ui threads.
+ * @param enabled
+ */
+ protected void doEnable(boolean enabled) {
+ mVoiceLabel.setEnabled(enabled);
+ mVoiceMode.setEnabled(enabled);
+
+ mDataLabel.setEnabled(enabled);
+ mDataMode.setEnabled(enabled);
+
+ mSpeedLabel.setEnabled(enabled);
+ mNetworkSpeed.setEnabled(enabled);
+
+ mLatencyLabel.setEnabled(enabled);
+ mNetworkLatency.setEnabled(enabled);
+
+ // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+ // if we don't need to.
+ if (mPhoneNumber.isEnabled() != enabled) {
+ mNumberLabel.setEnabled(enabled);
+ mPhoneNumber.setEnabled(enabled);
+ }
+
+ boolean valid = isValidPhoneNumber();
+
+ mVoiceButton.setEnabled(enabled && valid);
+ mSmsButton.setEnabled(enabled && valid);
+
+ boolean smsValid = enabled && valid && mSmsButton.getSelection();
+
+ // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+ // if we don't need to.
+ if (mSmsMessage.isEnabled() != smsValid) {
+ mMessageLabel.setEnabled(smsValid);
+ mSmsMessage.setEnabled(smsValid);
+ }
+ if (enabled == false) {
+ mSmsMessage.setText(""); //$NON-NLs-1$
+ }
+
+ mCallButton.setEnabled(enabled && valid);
+ mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection());
+
+ if (enabled == false) {
+ mVoiceMode.clearSelection();
+ mDataMode.clearSelection();
+ mNetworkSpeed.clearSelection();
+ mNetworkLatency.clearSelection();
+ if (mPhoneNumber.getText().length() > 0) {
+ mPhoneNumber.setText(""); //$NON-NLS-1$
+ }
+ }
+
+ // location controls
+ mLocationFolders.setEnabled(enabled);
+
+ mDecimalButton.setEnabled(enabled);
+ mSexagesimalButton.setEnabled(enabled);
+ mLongitudeControls.setEnabled(enabled);
+ mLatitudeControls.setEnabled(enabled);
+
+ mGpxUploadButton.setEnabled(enabled);
+ mGpxWayPointTable.setEnabled(enabled);
+ mGpxTrackTable.setEnabled(enabled);
+ mKmlUploadButton.setEnabled(enabled);
+ mKmlWayPointTable.setEnabled(enabled);
+ }
+
+ /**
+ * Returns the index of the combo item matching a specific GsmMode.
+ * @param mode
+ */
+ private int getGsmComboIndex(GsmMode mode) {
+ for (int i = 0 ; i < GSM_MODES.length; i++) {
+ String[] modes = GSM_MODES[i];
+ if (mode.getTag().equals(modes[1])) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Processes the result of a command sent to the console.
+ * @param result the result of the command.
+ */
+ private boolean processCommandResult(final String result) {
+ if (result != EmulatorConsole.RESULT_OK) {
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mParent.isDisposed() == false) {
+ MessageDialog.openError(mParent.getShell(), "Emulator Console",
+ result);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param track
+ */
+ private void playTrack(final Track track) {
+ // no need to synchronize this check, the worst that can happen, is we start the thread
+ // for nothing.
+ if (mEmulatorConsole != null) {
+ mPlayGpxButton.setImage(mPauseImage);
+ mPlayKmlButton.setImage(mPauseImage);
+ mPlayingTrack = true;
+
+ mPlayingThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ TrackPoint[] trackPoints = track.getPoints();
+ int count = trackPoints.length;
+
+ // get the start index.
+ int start = 0;
+ if (mPlayDirection == -1) {
+ start = count - 1;
+ }
+
+ for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // get the current point and send its location to
+ // the emulator.
+ final TrackPoint trackPoint = trackPoints[p];
+
+ synchronized (EmulatorControlPanel.this) {
+ if (mEmulatorConsole == null ||
+ processCommandResult(mEmulatorConsole.sendLocation(
+ trackPoint.getLongitude(), trackPoint.getLatitude(),
+ trackPoint.getElevation())) == false) {
+ return;
+ }
+ }
+
+ // if this is not the final point, then get the next one and
+ // compute the delta time
+ int nextIndex = p + mPlayDirection;
+ if (nextIndex >=0 && nextIndex < count) {
+ TrackPoint nextPoint = trackPoints[nextIndex];
+
+ long delta = nextPoint.getTime() - trackPoint.getTime();
+ if (delta < 0) {
+ delta = -delta;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ try {
+ sleep(delta / mSpeed);
+ } catch (InterruptedException e) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // we got interrupted, lets make sure we can play
+ do {
+ long waited = System.currentTimeMillis() - startTime;
+ long needToWait = delta / mSpeed;
+ if (waited < needToWait) {
+ try {
+ sleep(needToWait - waited);
+ } catch (InterruptedException e1) {
+ // we'll just loop and wait again if needed.
+ // unless we're supposed to stop
+ if (mPlayingTrack == false) {
+ return;
+ }
+ }
+ } else {
+ break;
+ }
+ } while (true);
+ }
+ }
+ }
+ } finally {
+ mPlayingTrack = false;
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mPlayGpxButton.isDisposed() == false) {
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayKmlButton.setImage(mPlayImage);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+ }
+ }
+ };
+
+ mPlayingThread.start();
+ }
+ }
+
+ private void playKml(final WayPoint[] trackPoints) {
+ // no need to synchronize this check, the worst that can happen, is we start the thread
+ // for nothing.
+ if (mEmulatorConsole != null) {
+ mPlayGpxButton.setImage(mPauseImage);
+ mPlayKmlButton.setImage(mPauseImage);
+ mPlayingTrack = true;
+
+ mPlayingThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ int count = trackPoints.length;
+
+ // get the start index.
+ int start = 0;
+ if (mPlayDirection == -1) {
+ start = count - 1;
+ }
+
+ for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // get the current point and send its location to
+ // the emulator.
+ WayPoint trackPoint = trackPoints[p];
+
+ synchronized (EmulatorControlPanel.this) {
+ if (mEmulatorConsole == null ||
+ processCommandResult(mEmulatorConsole.sendLocation(
+ trackPoint.getLongitude(), trackPoint.getLatitude(),
+ trackPoint.getElevation())) == false) {
+ return;
+ }
+ }
+
+ // if this is not the final point, then get the next one and
+ // compute the delta time
+ int nextIndex = p + mPlayDirection;
+ if (nextIndex >=0 && nextIndex < count) {
+
+ long delta = 1000; // 1 second
+ if (delta < 0) {
+ delta = -delta;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ try {
+ sleep(delta / mSpeed);
+ } catch (InterruptedException e) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // we got interrupted, lets make sure we can play
+ do {
+ long waited = System.currentTimeMillis() - startTime;
+ long needToWait = delta / mSpeed;
+ if (waited < needToWait) {
+ try {
+ sleep(needToWait - waited);
+ } catch (InterruptedException e1) {
+ // we'll just loop and wait again if needed.
+ // unless we're supposed to stop
+ if (mPlayingTrack == false) {
+ return;
+ }
+ }
+ } else {
+ break;
+ }
+ } while (true);
+ }
+ }
+ }
+ } finally {
+ mPlayingTrack = false;
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mPlayGpxButton.isDisposed() == false) {
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayKmlButton.setImage(mPlayImage);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+ }
+ }
+ };
+
+ mPlayingThread.start();
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java
new file mode 100644
index 0000000..fe3f438
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * {@link FindDialog} provides a text box where users can enter text that should be
+ * searched for in the target editor/view. The buttons "Find Previous" and "Find Next"
+ * allow users to search forwards/backwards. This dialog simply provides a front end for the user
+ * and the actual task of searching is delegated to the {@link IFindTarget}.
+ */
+public class FindDialog extends Dialog {
+ private Label mStatusLabel;
+ private Button mFindNext;
+ private Button mFindPrevious;
+ private final IFindTarget mTarget;
+ private Text mSearchText;
+ private String mPreviousSearchText;
+ private final int mDefaultButtonId;
+
+ /** Id of the "Find Next" button */
+ public static final int FIND_NEXT_ID = IDialogConstants.CLIENT_ID;
+
+ /** Id of the "Find Previous button */
+ public static final int FIND_PREVIOUS_ID = IDialogConstants.CLIENT_ID + 1;
+
+ public FindDialog(Shell shell, IFindTarget target) {
+ this(shell, target, FIND_PREVIOUS_ID);
+ }
+
+ /**
+ * Construct a find dialog.
+ * @param shell shell to use
+ * @param target delegate to be invoked on user action
+ * @param defaultButtonId one of {@code #FIND_NEXT_ID} or {@code #FIND_PREVIOUS_ID}.
+ */
+ public FindDialog(Shell shell, IFindTarget target, int defaultButtonId) {
+ super(shell);
+
+ mTarget = target;
+ mDefaultButtonId = defaultButtonId;
+
+ setShellStyle((getShellStyle() & ~SWT.APPLICATION_MODAL) | SWT.MODELESS);
+ setBlockOnOpen(true);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite panel = new Composite(parent, SWT.NONE);
+ panel.setLayout(new GridLayout(2, false));
+ panel.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Label lblMessage = new Label(panel, SWT.NONE);
+ lblMessage.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ lblMessage.setText("Find:");
+
+ mSearchText = new Text(panel, SWT.BORDER);
+ mSearchText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mSearchText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ boolean hasText = !mSearchText.getText().trim().isEmpty();
+ mFindNext.setEnabled(hasText);
+ mFindPrevious.setEnabled(hasText);
+ }
+ });
+
+ mStatusLabel = new Label(panel, SWT.NONE);
+ mStatusLabel.setForeground(getShell().getDisplay().getSystemColor(SWT.COLOR_DARK_RED));
+ GridData gd = new GridData();
+ gd.horizontalSpan = 2;
+ gd.grabExcessHorizontalSpace = true;
+ mStatusLabel.setLayoutData(gd);
+
+ return panel;
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, false);
+
+ mFindNext = createButton(parent, FIND_NEXT_ID, "Find Next",
+ mDefaultButtonId == FIND_NEXT_ID);
+ mFindPrevious = createButton(parent, FIND_PREVIOUS_ID, "Find Previous",
+ mDefaultButtonId != FIND_NEXT_ID);
+ mFindNext.setEnabled(false);
+ mFindPrevious.setEnabled(false);
+ }
+
+ @Override
+ protected void buttonPressed(int buttonId) {
+ if (buttonId == IDialogConstants.CLOSE_ID) {
+ close();
+ return;
+ }
+
+ if (buttonId == FIND_PREVIOUS_ID || buttonId == FIND_NEXT_ID) {
+ if (mTarget != null) {
+ String searchText = mSearchText.getText();
+ boolean newSearch = !searchText.equals(mPreviousSearchText);
+ mPreviousSearchText = searchText;
+ boolean searchForward = buttonId == FIND_NEXT_ID;
+
+ boolean hasMatches = mTarget.findAndSelect(searchText, newSearch, searchForward);
+ if (!hasMatches) {
+ mStatusLabel.setText("String not found");
+ mStatusLabel.pack();
+ } else {
+ mStatusLabel.setText("");
+ }
+ }
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
new file mode 100644
index 0000000..d0af8b0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
@@ -0,0 +1,1310 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+import com.android.ddmlib.Log;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.CategoryLabelPositions;
+import org.jfree.chart.labels.CategoryToolTipGenerator;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.Plot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.CategoryItemRenderer;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Base class for our information panels.
+ */
+public final class HeapPanel extends BaseHeapPanel {
+ private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$
+
+ /* args to setUpdateStatus() */
+ private static final int NOT_SELECTED = 0;
+ private static final int NOT_ENABLED = 1;
+ private static final int ENABLED = 2;
+
+ /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+ * Native+1 at least. We also need 2 more entries for free area and expansion area. */
+ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+ private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+ private static final PaletteData mMapPalette = createPalette();
+
+ private static final boolean DISPLAY_HEAP_BITMAP = false;
+ private static final boolean DISPLAY_HILBERT_BITMAP = false;
+
+ private static final int PLACEHOLDER_HILBERT_SIZE = 200;
+ private static final int PLACEHOLDER_LINEAR_V_SIZE = 100;
+ private static final int PLACEHOLDER_LINEAR_H_SIZE = 300;
+
+ private static final int[] ZOOMS = {100, 50, 25};
+
+ private static final NumberFormat sByteFormatter = NumberFormat.getInstance();
+ private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance();
+ private static final NumberFormat sCountFormatter = NumberFormat.getInstance();
+
+ static {
+ sByteFormatter.setMinimumFractionDigits(0);
+ sByteFormatter.setMaximumFractionDigits(1);
+ sLargeByteFormatter.setMinimumFractionDigits(3);
+ sLargeByteFormatter.setMaximumFractionDigits(3);
+
+ sCountFormatter.setGroupingUsed(true);
+ }
+
+ private Display mDisplay;
+
+ private Composite mTop; // real top
+ private Label mUpdateStatus;
+ private Table mHeapSummary;
+ private Combo mDisplayMode;
+
+ //private ScrolledComposite mScrolledComposite;
+
+ private Composite mDisplayBase; // base of the displays.
+ private StackLayout mDisplayStack;
+
+ private Composite mStatisticsBase;
+ private Table mStatisticsTable;
+ private JFreeChart mChart;
+ private ChartComposite mChartComposite;
+ private Button mGcButton;
+ private DefaultCategoryDataset mAllocCountDataSet;
+
+ private Composite mLinearBase;
+ private Label mLinearHeapImage;
+
+ private Composite mHilbertBase;
+ private Label mHilbertHeapImage;
+ private Group mLegend;
+ private Combo mZoom;
+
+ /** Image used for the hilbert display. Since we recreate a new image every time, we
+ * keep this one around to dispose it. */
+ private Image mHilbertImage;
+ private Image mLinearImage;
+ private Composite[] mLayout;
+
+ /*
+ * Create color palette for map. Set up titles for legend.
+ */
+ private static PaletteData createPalette() {
+ RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+ colors[0]
+ = new RGB(192, 192, 192); // non-heap pixels are gray
+ mMapLegend[0]
+ = "(heap expansion area)";
+
+ colors[1]
+ = new RGB(0, 0, 0); // free chunks are black
+ mMapLegend[1]
+ = "free";
+
+ colors[HeapSegmentElement.KIND_OBJECT + 2]
+ = new RGB(0, 0, 255); // objects are blue
+ mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+ = "data object";
+
+ colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = new RGB(0, 255, 0); // class objects are green
+ mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = "class object";
+
+ colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = new RGB(255, 0, 0); // byte/bool arrays are red
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = "1-byte array (byte[], boolean[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = new RGB(255, 128, 0); // short/char arrays are orange
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = "2-byte array (short[], char[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = new RGB(255, 255, 0); // obj/int/float arrays are yellow
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = "4-byte array (object[], int[], float[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = new RGB(255, 128, 128); // long/double arrays are pink
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = "8-byte array (long[], double[])";
+
+ colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = new RGB(255, 0, 255); // unknown objects are cyan
+ mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = "unknown object";
+
+ colors[HeapSegmentElement.KIND_NATIVE + 2]
+ = new RGB(64, 64, 64); // native objects are dark gray
+ mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+ = "non-Java object";
+
+ return new PaletteData(colors);
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE ||
+ (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) {
+ try {
+ mTop.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ clientSelected();
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed (app is quitting most likely), we do nothing.
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mTop.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ Log.d("ddms", "HeapPanel: changed " + client);
+
+ if (client != null) {
+ ClientData cd = client.getClientData();
+
+ if (client.isHeapUpdateEnabled()) {
+ mGcButton.setEnabled(true);
+ mDisplayMode.setEnabled(true);
+ setUpdateStatus(ENABLED);
+ } else {
+ setUpdateStatus(NOT_ENABLED);
+ mGcButton.setEnabled(false);
+ mDisplayMode.setEnabled(false);
+ }
+
+ fillSummaryTable(cd);
+
+ int mode = mDisplayMode.getSelectionIndex();
+ if (mode == 0) {
+ fillDetailedTable(client, false /* forceRedraw */);
+ } else {
+ if (DISPLAY_HEAP_BITMAP) {
+ renderHeapData(cd, mode - 1, false /* forceRedraw */);
+ }
+ }
+ } else {
+ mGcButton.setEnabled(false);
+ mDisplayMode.setEnabled(false);
+ fillSummaryTable(null);
+ fillDetailedTable(null, true);
+ setUpdateStatus(NOT_SELECTED);
+ }
+
+ // sizes of things change frequently, so redo layout
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ GridLayout gl;
+
+ mTop = new Composite(parent, SWT.NONE);
+ mTop.setLayout(new GridLayout(1, false));
+ mTop.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mUpdateStatus = new Label(mTop, SWT.NONE);
+ setUpdateStatus(NOT_SELECTED);
+
+ Composite summarySection = new Composite(mTop, SWT.NONE);
+ summarySection.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mHeapSummary = createSummaryTable(summarySection);
+ mGcButton = new Button(summarySection, SWT.PUSH);
+ mGcButton.setText("Cause GC");
+ mGcButton.setEnabled(false);
+ mGcButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.executeGarbageCollector();
+ }
+ }
+ });
+
+ Composite comboSection = new Composite(mTop, SWT.NONE);
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ comboSection.setLayout(gl);
+
+ Label displayLabel = new Label(comboSection, SWT.NONE);
+ displayLabel.setText("Display: ");
+
+ mDisplayMode = new Combo(comboSection, SWT.READ_ONLY);
+ mDisplayMode.setEnabled(false);
+ mDisplayMode.add("Stats");
+ if (DISPLAY_HEAP_BITMAP) {
+ mDisplayMode.add("Linear");
+ if (DISPLAY_HILBERT_BITMAP) {
+ mDisplayMode.add("Hilbert");
+ }
+ }
+
+ // the base of the displays.
+ mDisplayBase = new Composite(mTop, SWT.NONE);
+ mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mDisplayStack = new StackLayout();
+ mDisplayBase.setLayout(mDisplayStack);
+
+ // create the statistics display
+ mStatisticsBase = new Composite(mDisplayBase, SWT.NONE);
+ //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mStatisticsBase.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mDisplayStack.topControl = mStatisticsBase;
+
+ mStatisticsTable = createDetailedTable(mStatisticsBase);
+ mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createChart();
+
+ //create the linear composite
+ mLinearBase = new Composite(mDisplayBase, SWT.NONE);
+ //mLinearBase.setLayoutData(new GridData());
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ mLinearBase.setLayout(gl);
+
+ {
+ mLinearHeapImage = new Label(mLinearBase, SWT.NONE);
+ mLinearHeapImage.setLayoutData(new GridData());
+ mLinearHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
+ PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE,
+ mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+ // create a composite to contain the bottom part (legend)
+ Composite bottomSection = new Composite(mLinearBase, SWT.NONE);
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ bottomSection.setLayout(gl);
+
+ createLegend(bottomSection);
+ }
+
+/*
+ mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL);
+ mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mScrolledComposite.setExpandHorizontal(true);
+ mScrolledComposite.setExpandVertical(true);
+ mScrolledComposite.setContent(mDisplayBase);
+*/
+
+
+ // create the hilbert display.
+ mHilbertBase = new Composite(mDisplayBase, SWT.NONE);
+ //mHilbertBase.setLayoutData(new GridData());
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ mHilbertBase.setLayout(gl);
+
+ if (DISPLAY_HILBERT_BITMAP) {
+ mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE);
+ mHilbertHeapImage.setLayoutData(new GridData());
+ mHilbertHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
+ PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE,
+ mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+ // create a composite to contain the right part (legend + zoom)
+ Composite rightSection = new Composite(mHilbertBase, SWT.NONE);
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ rightSection.setLayout(gl);
+
+ Composite zoomComposite = new Composite(rightSection, SWT.NONE);
+ gl = new GridLayout(2, false);
+ zoomComposite.setLayout(gl);
+
+ Label l = new Label(zoomComposite, SWT.NONE);
+ l.setText("Zoom:");
+ mZoom = new Combo(zoomComposite, SWT.READ_ONLY);
+ for (int z : ZOOMS) {
+ mZoom.add(String.format("%1$d%%", z)); //$NON-NLS-1$
+ }
+
+ mZoom.select(0);
+ mZoom.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setLegendText(mZoom.getSelectionIndex());
+ Client client = getCurrentClient();
+ if (client != null) {
+ renderHeapData(client.getClientData(), 1, true);
+ mTop.pack();
+ }
+ }
+ });
+
+ createLegend(rightSection);
+ }
+ mHilbertBase.pack();
+
+ mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase };
+ mDisplayMode.select(0);
+ mDisplayMode.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = mDisplayMode.getSelectionIndex();
+ Client client = getCurrentClient();
+
+ if (client != null) {
+ if (index == 0) {
+ fillDetailedTable(client, true /* forceRedraw */);
+ } else {
+ renderHeapData(client.getClientData(), index-1, true /* forceRedraw */);
+ }
+ }
+
+ mDisplayStack.topControl = mLayout[index];
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+ }
+ });
+
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+
+ return mTop;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mHeapSummary.setFocus();
+ }
+
+
+ private Table createSummaryTable(Composite base) {
+ Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+ tab.setHeaderVisible(true);
+ tab.setLinesVisible(true);
+
+ TableColumn col;
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("ID");
+ col.pack();
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); //$NON-NLS-1$
+ col.pack();
+ col.setText("Heap Size");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); //$NON-NLS-1$
+ col.pack();
+ col.setText("Allocated");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); //$NON-NLS-1$
+ col.pack();
+ col.setText("Free");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.00%"); //$NON-NLS-1$
+ col.pack();
+ col.setText("% Used");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000,000,000"); //$NON-NLS-1$
+ col.pack();
+ col.setText("# Objects");
+
+ // make sure there is always one empty item so that one table row is always displayed.
+ TableItem item = new TableItem(tab, SWT.NONE);
+ item.setText("");
+
+ return tab;
+ }
+
+ private Table createDetailedTable(Composite base) {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+ tab.setHeaderVisible(true);
+ tab.setLinesVisible(true);
+
+ TableHelper.createTableColumn(tab, "Type", SWT.LEFT,
+ "4-byte array (object[], int[], float[])", //$NON-NLS-1$
+ PREFS_STATS_COL_TYPE, store);
+
+ TableHelper.createTableColumn(tab, "Count", SWT.RIGHT,
+ "00,000", //$NON-NLS-1$
+ PREFS_STATS_COL_COUNT, store);
+
+ TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_SIZE, store);
+
+ TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_SMALLEST, store);
+
+ TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_LARGEST, store);
+
+ TableHelper.createTableColumn(tab, "Median", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_MEDIAN, store);
+
+ TableHelper.createTableColumn(tab, "Average", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_AVERAGE, store);
+
+ tab.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+
+ Client client = getCurrentClient();
+ if (client != null) {
+ int index = mStatisticsTable.getSelectionIndex();
+ TableItem item = mStatisticsTable.getItem(index);
+
+ if (item != null) {
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+ client.getClientData().getVmHeapData().getProcessedHeapMap();
+
+ ArrayList<HeapSegmentElement> list = heapMap.get(item.getData());
+ if (list != null) {
+ showChart(list);
+ }
+ }
+ }
+
+ }
+ });
+
+ return tab;
+ }
+
+ /**
+ * Creates the chart below the statistics table
+ */
+ private void createChart() {
+ mAllocCountDataSet = new DefaultCategoryDataset();
+ mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet,
+ PlotOrientation.VERTICAL, false, true, false);
+
+ // get the font to make a proper title. We need to convert the swt font,
+ // into an awt font.
+ Font f = mStatisticsBase.getFont();
+ FontData[] fData = f.getFontData();
+
+ // event though on Mac OS there could be more than one fontData, we'll only use
+ // the first one.
+ FontData firstFontData = fData[0];
+
+ java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(),
+ firstFontData, true /* ensureSameSize */);
+
+ mChart.setTitle(new TextTitle("Allocation count per size", awtFont));
+
+ Plot plot = mChart.getPlot();
+ if (plot instanceof CategoryPlot) {
+ // get the plot
+ CategoryPlot categoryPlot = (CategoryPlot)plot;
+
+ // set the domain axis to draw labels that are displayed even with many values.
+ CategoryAxis domainAxis = categoryPlot.getDomainAxis();
+ domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90);
+
+ CategoryItemRenderer renderer = categoryPlot.getRenderer();
+ renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() {
+ @Override
+ public String generateToolTip(CategoryDataset dataset, int row, int column) {
+ // get the key for the size of the allocation
+ ByteLong columnKey = (ByteLong)dataset.getColumnKey(column);
+ String rowKey = (String)dataset.getRowKey(row);
+ Number value = dataset.getValue(rowKey, columnKey);
+
+ return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey,
+ columnKey.getValue());
+ }
+ });
+ }
+ mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000, // max draw width. We don't want it to zoom, so we put a big number
+ 3000, // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ false, // zoom
+ true); // tooltips
+
+ mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ }
+
+ private static String prettyByteCount(long bytes) {
+ double fracBytes = bytes;
+ String units = " B";
+ if (fracBytes < 1024) {
+ return sByteFormatter.format(fracBytes) + units;
+ } else {
+ fracBytes /= 1024;
+ units = " KB";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = " MB";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = " GB";
+ }
+
+ return sLargeByteFormatter.format(fracBytes) + units;
+ }
+
+ private static String approximateByteCount(long bytes) {
+ double fracBytes = bytes;
+ String units = "";
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "K";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "M";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "G";
+ }
+
+ return sByteFormatter.format(fracBytes) + units;
+ }
+
+ private static String addCommasToNumber(long num) {
+ return sCountFormatter.format(num);
+ }
+
+ private static String fractionalPercent(long num, long denom) {
+ double val = (double)num / (double)denom;
+ val *= 100;
+
+ NumberFormat nf = NumberFormat.getInstance();
+ nf.setMinimumFractionDigits(2);
+ nf.setMaximumFractionDigits(2);
+ return nf.format(val) + "%";
+ }
+
+ private void fillSummaryTable(ClientData cd) {
+ if (mHeapSummary.isDisposed()) {
+ return;
+ }
+
+ mHeapSummary.setRedraw(false);
+ mHeapSummary.removeAll();
+
+ int numRows = 0;
+ if (cd != null) {
+ synchronized (cd) {
+ Iterator<Integer> iter = cd.getVmHeapIds();
+
+ while (iter.hasNext()) {
+ numRows++;
+ Integer id = iter.next();
+ Map<String, Long> heapInfo = cd.getVmHeapInfo(id);
+ if (heapInfo == null) {
+ continue;
+ }
+ long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES);
+ long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED);
+ long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED);
+
+ TableItem item = new TableItem(mHeapSummary, SWT.NONE);
+ item.setText(0, id.toString());
+
+ item.setText(1, prettyByteCount(sizeInBytes));
+ item.setText(2, prettyByteCount(bytesAllocated));
+ item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated));
+ item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes));
+ item.setText(5, addCommasToNumber(objectsAllocated));
+ }
+ }
+ }
+
+ if (numRows == 0) {
+ // make sure there is always one empty item so that one table row is always displayed.
+ TableItem item = new TableItem(mHeapSummary, SWT.NONE);
+ item.setText("");
+ }
+
+ mHeapSummary.pack();
+ mHeapSummary.setRedraw(true);
+ }
+
+ private void fillDetailedTable(Client client, boolean forceRedraw) {
+ // first check if the client is invalid or heap updates are not enabled.
+ if (client == null || client.isHeapUpdateEnabled() == false) {
+ mStatisticsTable.removeAll();
+ showChart(null);
+ return;
+ }
+
+ ClientData cd = client.getClientData();
+
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+ // no change, we return.
+ return;
+ }
+
+ heapMap = cd.getVmHeapData().getProcessedHeapMap();
+ }
+
+ // we have new data, lets display it.
+
+ // First, get the current selection, and its key.
+ int index = mStatisticsTable.getSelectionIndex();
+ Integer selectedKey = null;
+ if (index != -1) {
+ selectedKey = (Integer)mStatisticsTable.getItem(index).getData();
+ }
+
+ // disable redraws and remove all from the table.
+ mStatisticsTable.setRedraw(false);
+ mStatisticsTable.removeAll();
+
+ if (heapMap != null) {
+ int selectedIndex = -1;
+ ArrayList<HeapSegmentElement> selectedList = null;
+
+ // get the keys
+ Set<Integer> keys = heapMap.keySet();
+ int iter = 0; // use a manual iter int because Set<?> doesn't have an index
+ // based accessor.
+ for (Integer key : keys) {
+ ArrayList<HeapSegmentElement> list = heapMap.get(key);
+
+ // check if this is the key that is supposed to be selected
+ if (key.equals(selectedKey)) {
+ selectedIndex = iter;
+ selectedList = list;
+ }
+ iter++;
+
+ TableItem item = new TableItem(mStatisticsTable, SWT.NONE);
+ item.setData(key);
+
+ // get the type
+ item.setText(0, mMapLegend[key]);
+
+ // set the count, smallest, largest
+ int count = list.size();
+ item.setText(1, addCommasToNumber(count));
+
+ if (count > 0) {
+ item.setText(3, prettyByteCount(list.get(0).getLength()));
+ item.setText(4, prettyByteCount(list.get(count-1).getLength()));
+
+ int median = count / 2;
+ HeapSegmentElement element = list.get(median);
+ long size = element.getLength();
+ item.setText(5, prettyByteCount(size));
+
+ long totalSize = 0;
+ for (int i = 0 ; i < count; i++) {
+ element = list.get(i);
+
+ size = element.getLength();
+ totalSize += size;
+ }
+
+ // set the average and total
+ item.setText(2, prettyByteCount(totalSize));
+ item.setText(6, prettyByteCount(totalSize / count));
+ }
+ }
+
+ mStatisticsTable.setRedraw(true);
+
+ if (selectedIndex != -1) {
+ mStatisticsTable.setSelection(selectedIndex);
+ showChart(selectedList);
+ } else {
+ showChart(null);
+ }
+ } else {
+ mStatisticsTable.setRedraw(true);
+ }
+ }
+
+ private static class ByteLong implements Comparable<ByteLong> {
+ private long mValue;
+
+ private ByteLong(long value) {
+ mValue = value;
+ }
+
+ public long getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return approximateByteCount(mValue);
+ }
+
+ @Override
+ public int compareTo(ByteLong other) {
+ if (mValue != other.mValue) {
+ return mValue < other.mValue ? -1 : 1;
+ }
+ return 0;
+ }
+
+ }
+
+ /**
+ * Fills the chart with the content of the list of {@link HeapSegmentElement}.
+ */
+ private void showChart(ArrayList<HeapSegmentElement> list) {
+ mAllocCountDataSet.clear();
+
+ if (list != null) {
+ String rowKey = "Alloc Count";
+
+ long currentSize = -1;
+ int currentCount = 0;
+ for (HeapSegmentElement element : list) {
+ if (element.getLength() != currentSize) {
+ if (currentSize != -1) {
+ ByteLong columnKey = new ByteLong(currentSize);
+ mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+ }
+
+ currentSize = element.getLength();
+ currentCount = 1;
+ } else {
+ currentCount++;
+ }
+ }
+
+ // add the last item
+ if (currentSize != -1) {
+ ByteLong columnKey = new ByteLong(currentSize);
+ mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+ }
+ }
+ }
+
+ /*
+ * Add a color legend to the specified table.
+ */
+ private void createLegend(Composite parent) {
+ mLegend = new Group(parent, SWT.NONE);
+ mLegend.setText(getLegendText(0));
+
+ mLegend.setLayout(new GridLayout(2, false));
+
+ RGB[] colors = mMapPalette.colors;
+
+ for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) {
+ Image tmpImage = createColorRect(parent.getDisplay(), colors[i]);
+
+ Label l = new Label(mLegend, SWT.NONE);
+ l.setImage(tmpImage);
+
+ l = new Label(mLegend, SWT.NONE);
+ l.setText(mMapLegend[i]);
+ }
+ }
+
+ private String getLegendText(int level) {
+ int bytes = 8 * (100 / ZOOMS[level]);
+
+ return String.format("Key (1 pixel = %1$d bytes)", bytes);
+ }
+
+ private void setLegendText(int level) {
+ mLegend.setText(getLegendText(level));
+
+ }
+
+ /*
+ * Create a nice rectangle in the specified color.
+ */
+ private Image createColorRect(Display display, RGB color) {
+ int width = 32;
+ int height = 16;
+
+ Image img = new Image(display, width, height);
+ GC gc = new GC(img);
+ gc.setBackground(new Color(display, color));
+ gc.fillRectangle(0, 0, width, height);
+ gc.dispose();
+ return img;
+ }
+
+
+ /*
+ * Are updates enabled?
+ */
+ private void setUpdateStatus(int status) {
+ switch (status) {
+ case NOT_SELECTED:
+ mUpdateStatus.setText("Select a client to see heap updates");
+ break;
+ case NOT_ENABLED:
+ mUpdateStatus.setText("Heap updates are " +
+ "NOT ENABLED for this client");
+ break;
+ case ENABLED:
+ mUpdateStatus.setText("Heap updates will happen after " +
+ "every GC for this client");
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
+ mUpdateStatus.pack();
+ }
+
+
+ /**
+ * Return the closest power of two greater than or equal to value.
+ *
+ * @param value the return value will be >= value
+ * @return a power of two >= value. If value > 2^31, 2^31 is returned.
+ */
+//xxx use Integer.highestOneBit() or numberOfLeadingZeros().
+ private int nextPow2(int value) {
+ for (int i = 31; i >= 0; --i) {
+ if ((value & (1<<i)) != 0) {
+ if (i < 31) {
+ return 1<<(i + 1);
+ } else {
+ return 1<<31;
+ }
+ }
+ }
+ return 0;
+ }
+
+ private int zOrderData(ImageData id, byte pixData[]) {
+ int maxX = 0;
+ for (int i = 0; i < pixData.length; i++) {
+ /* Tread the pixData index as a z-order curve index and
+ * decompose into Cartesian coordinates.
+ */
+ int x = (i & 1) |
+ ((i >>> 2) & 1) << 1 |
+ ((i >>> 4) & 1) << 2 |
+ ((i >>> 6) & 1) << 3 |
+ ((i >>> 8) & 1) << 4 |
+ ((i >>> 10) & 1) << 5 |
+ ((i >>> 12) & 1) << 6 |
+ ((i >>> 14) & 1) << 7 |
+ ((i >>> 16) & 1) << 8 |
+ ((i >>> 18) & 1) << 9 |
+ ((i >>> 20) & 1) << 10 |
+ ((i >>> 22) & 1) << 11 |
+ ((i >>> 24) & 1) << 12 |
+ ((i >>> 26) & 1) << 13 |
+ ((i >>> 28) & 1) << 14 |
+ ((i >>> 30) & 1) << 15;
+ int y = ((i >>> 1) & 1) << 0 |
+ ((i >>> 3) & 1) << 1 |
+ ((i >>> 5) & 1) << 2 |
+ ((i >>> 7) & 1) << 3 |
+ ((i >>> 9) & 1) << 4 |
+ ((i >>> 11) & 1) << 5 |
+ ((i >>> 13) & 1) << 6 |
+ ((i >>> 15) & 1) << 7 |
+ ((i >>> 17) & 1) << 8 |
+ ((i >>> 19) & 1) << 9 |
+ ((i >>> 21) & 1) << 10 |
+ ((i >>> 23) & 1) << 11 |
+ ((i >>> 25) & 1) << 12 |
+ ((i >>> 27) & 1) << 13 |
+ ((i >>> 29) & 1) << 14 |
+ ((i >>> 31) & 1) << 15;
+ try {
+ id.setPixel(x, y, pixData[i]);
+ if (x > maxX) {
+ maxX = x;
+ }
+ } catch (IllegalArgumentException ex) {
+ System.out.println("bad pixels: i " + i +
+ ", w " + id.width +
+ ", h " + id.height +
+ ", x " + x +
+ ", y " + y);
+ throw ex;
+ }
+ }
+ return maxX;
+ }
+
+ private final static int HILBERT_DIR_N = 0;
+ private final static int HILBERT_DIR_S = 1;
+ private final static int HILBERT_DIR_E = 2;
+ private final static int HILBERT_DIR_W = 3;
+
+ private void hilbertWalk(ImageData id, InputStream pixData,
+ int order, int x, int y, int dir)
+ throws IOException {
+ if (x >= id.width || y >= id.height) {
+ return;
+ } else if (order == 0) {
+ try {
+ int p = pixData.read();
+ if (p >= 0) {
+ // flip along x=y axis; assume width == height
+ id.setPixel(y, x, p);
+
+ /* Skanky; use an otherwise-unused ImageData field
+ * to keep track of the max x,y used. Note that x and y are inverted.
+ */
+ if (y > id.x) {
+ id.x = y;
+ }
+ if (x > id.y) {
+ id.y = x;
+ }
+ }
+//xxx just give up; don't bother walking the rest of the image
+ } catch (IllegalArgumentException ex) {
+ System.out.println("bad pixels: order " + order +
+ ", dir " + dir +
+ ", w " + id.width +
+ ", h " + id.height +
+ ", x " + x +
+ ", y " + y);
+ throw ex;
+ }
+ } else {
+ order--;
+ int delta = 1 << order;
+ int nextX = x + delta;
+ int nextY = y + delta;
+
+ switch (dir) {
+ case HILBERT_DIR_E:
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S);
+ break;
+ case HILBERT_DIR_N:
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W);
+ break;
+ case HILBERT_DIR_S:
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E);
+ break;
+ case HILBERT_DIR_W:
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N);
+ break;
+ default:
+ throw new RuntimeException("Unexpected Hilbert direction " +
+ dir);
+ }
+ }
+ }
+
+ private Point hilbertOrderData(ImageData id, byte pixData[]) {
+
+ int order = 0;
+ for (int n = 1; n < id.width; n *= 2) {
+ order++;
+ }
+ /* Skanky; use an otherwise-unused ImageData field
+ * to keep track of maxX.
+ */
+ Point p = new Point(0,0);
+ int oldIdX = id.x;
+ int oldIdY = id.y;
+ id.x = id.y = 0;
+ try {
+ hilbertWalk(id, new ByteArrayInputStream(pixData),
+ order, 0, 0, HILBERT_DIR_E);
+ p.x = id.x;
+ p.y = id.y;
+ } catch (IOException ex) {
+ System.err.println("Exception during hilbertWalk()");
+ p.x = id.height;
+ p.y = id.width;
+ }
+ id.x = oldIdX;
+ id.y = oldIdY;
+ return p;
+ }
+
+ private ImageData createHilbertHeapImage(byte pixData[]) {
+ int w, h;
+
+ // Pick an image size that the largest of heaps will fit into.
+ w = (int)Math.sqrt(((16 * 1024 * 1024)/8));
+
+ // Space-filling curves require a power-of-2 width.
+ w = nextPow2(w);
+ h = w;
+
+ // Create the heap image.
+ ImageData id = new ImageData(w, h, 8, mMapPalette);
+
+ // Copy the data into the image
+ //int maxX = zOrderData(id, pixData);
+ Point maxP = hilbertOrderData(id, pixData);
+
+ // update the max size to make it a round number once the zoom is applied
+ int factor = 100 / ZOOMS[mZoom.getSelectionIndex()];
+ if (factor != 1) {
+ int tmp = maxP.x % factor;
+ if (tmp != 0) {
+ maxP.x += factor - tmp;
+ }
+
+ tmp = maxP.y % factor;
+ if (tmp != 0) {
+ maxP.y += factor - tmp;
+ }
+ }
+
+ if (maxP.y < id.height) {
+ // Crop the image down to the interesting part.
+ id = new ImageData(id.width, maxP.y, id.depth, id.palette,
+ id.scanlinePad, id.data);
+ }
+
+ if (maxP.x < id.width) {
+ // crop the image again. A bit trickier this time.
+ ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette);
+
+ int[] buffer = new int[maxP.x];
+ for (int l = 0 ; l < id.height; l++) {
+ id.getPixels(0, l, maxP.x, buffer, 0);
+ croppedId.setPixels(0, l, maxP.x, buffer, 0);
+ }
+
+ id = croppedId;
+ }
+
+ // apply the zoom
+ if (factor != 1) {
+ id = id.scaledTo(id.width / factor, id.height / factor);
+ }
+
+ return id;
+ }
+
+ /**
+ * Convert the raw heap data to an image. We know we're running in
+ * the UI thread, so we can issue graphics commands directly.
+ *
+ * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html
+ *
+ * @param cd The client data
+ * @param mode The display mode. 0 = linear, 1 = hilbert.
+ * @param forceRedraw
+ */
+ private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) {
+ Image image;
+
+ byte[] pixData;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+ // no change, we return.
+ return;
+ }
+
+ pixData = getSerializedData();
+ }
+
+ if (pixData != null) {
+ ImageData id;
+ if (mode == 1) {
+ id = createHilbertHeapImage(pixData);
+ } else {
+ id = createLinearHeapImage(pixData, 200, mMapPalette);
+ }
+
+ image = new Image(mDisplay, id);
+ } else {
+ // Render a placeholder image.
+ int width, height;
+ if (mode == 1) {
+ width = height = PLACEHOLDER_HILBERT_SIZE;
+ } else {
+ width = PLACEHOLDER_LINEAR_H_SIZE;
+ height = PLACEHOLDER_LINEAR_V_SIZE;
+ }
+ image = new Image(mDisplay, width, height);
+ GC gc = new GC(image);
+ gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED));
+ gc.drawLine(0, 0, width-1, height-1);
+ gc.dispose();
+ gc = null;
+ }
+
+ // set the new image
+
+ if (mode == 1) {
+ if (mHilbertImage != null) {
+ mHilbertImage.dispose();
+ }
+
+ mHilbertImage = image;
+ mHilbertHeapImage.setImage(mHilbertImage);
+ mHilbertHeapImage.pack(true);
+ mHilbertBase.layout();
+ mHilbertBase.pack(true);
+ } else {
+ if (mLinearImage != null) {
+ mLinearImage.dispose();
+ }
+
+ mLinearImage = image;
+ mLinearHeapImage.setImage(mLinearImage);
+ mLinearHeapImage.pack(true);
+ mLinearBase.layout();
+ mLinearBase.pack(true);
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mHeapSummary);
+ }
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java
new file mode 100644
index 0000000..9aa6943
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+public interface IFindTarget {
+ boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java
new file mode 100644
index 0000000..37dd9a0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.swt.dnd.Clipboard;
+
+/**
+ * An object listening to focus change in Table objects.<br>
+ * For application not relying on a RCP to provide menu changes based on focus,
+ * this class allows to get monitor the focus change of several Table widget
+ * and update the menu action accordingly.
+ */
+public interface ITableFocusListener {
+
+ public interface IFocusedTableActivator {
+ public void copy(Clipboard clipboard);
+
+ public void selectAll();
+ }
+
+ public void focusGained(IFocusedTableActivator activator);
+
+ public void focusLost(IFocusedTableActivator activator);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java
new file mode 100644
index 0000000..fd480f6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+
+/**
+ * Class to load images stored in a jar file.
+ * All images are loaded from /images/<var>filename</var>
+ *
+ * Because Java requires to know the jar file in which to load the image from, a class is required
+ * when getting the instance. Instances are cached and associated to the class passed to
+ * {@link #getLoader(Class)}.
+ *
+ * {@link #getDdmUiLibLoader()} use {@link ImageLoader#getClass()} as the class. This is to be used
+ * to load images from ddmuilib.
+ *
+ * Loaded images are stored so that 2 calls with the same filename will return the same object.
+ * This also means that {@link Image} object returned by the loader should never be disposed.
+ *
+ */
+public class ImageLoader {
+
+ private static final String PATH = "/images/"; //$NON-NLS-1$
+
+ private final HashMap<String, Image> mLoadedImages = new HashMap<String, Image>();
+ private static final HashMap<Class<?>, ImageLoader> mInstances =
+ new HashMap<Class<?>, ImageLoader>();
+ private final Class<?> mClass;
+
+ /**
+ * Private constructor, creating an instance associated with a class.
+ * The class is used to identify which jar file the images are loaded from.
+ */
+ private ImageLoader(Class<?> theClass) {
+ if (theClass == null) {
+ theClass = ImageLoader.class;
+ }
+ mClass = theClass;
+ }
+
+ /**
+ * Returns the {@link ImageLoader} instance to load images from ddmuilib.jar
+ */
+ public static ImageLoader getDdmUiLibLoader() {
+ return getLoader(null);
+ }
+
+ /**
+ * Returns an {@link ImageLoader} to load images based on a given class.
+ *
+ * The loader will load images from the jar from which the class was loaded. using
+ * {@link Class#getResource(String)} and {@link Class#getResourceAsStream(String)}.
+ *
+ * Since all images are loaded using the path /images/<var>filename</var>, any class from the
+ * jar will work. However since the loader is cached and reused when the query provides the same
+ * class instance, and since the loader will also cache the loaded images, it is recommended
+ * to always use the same class for a given Jar file.
+ *
+ */
+ public static ImageLoader getLoader(Class<?> theClass) {
+ ImageLoader instance = mInstances.get(theClass);
+ if (instance == null) {
+ instance = new ImageLoader(theClass);
+ mInstances.put(theClass, instance);
+ }
+
+ return instance;
+ }
+
+ /**
+ * Disposes all images for all instances.
+ * This should only be called when the program exits.
+ */
+ public static void dispose() {
+ for (ImageLoader loader : mInstances.values()) {
+ loader.doDispose();
+ }
+ }
+
+ private synchronized void doDispose() {
+ for (Image image : mLoadedImages.values()) {
+ image.dispose();
+ }
+
+ mLoadedImages.clear();
+ }
+
+ /**
+ * Returns an {@link ImageDescriptor} for a given filename.
+ *
+ * This searches for an image located at /images/<var>filename</var>.
+ *
+ * @param filename the filename of the image to load.
+ */
+ public ImageDescriptor loadDescriptor(String filename) {
+ URL url = mClass.getResource(PATH + filename);
+ // TODO cache in a map
+ return ImageDescriptor.createFromURL(url);
+ }
+
+ /**
+ * Returns an {@link Image} for a given filename.
+ *
+ * This searches for an image located at /images/<var>filename</var>.
+ *
+ * @param filename the filename of the image to load.
+ * @param display the Display object
+ */
+ public synchronized Image loadImage(String filename, Display display) {
+ Image img = mLoadedImages.get(filename);
+ if (img == null) {
+ String tmp = PATH + filename;
+ InputStream imageStream = mClass.getResourceAsStream(tmp);
+
+ if (imageStream != null) {
+ img = new Image(display, imageStream);
+ mLoadedImages.put(filename, img);
+ }
+
+ if (img == null) {
+ throw new RuntimeException("Failed to load " + tmp);
+ }
+ }
+
+ return img;
+ }
+
+ /**
+ * Loads an image from a resource. This method used a class to locate the
+ * resources, and then load the filename from /images inside the resources.<br>
+ * Extra parameters allows for creation of a replacement image of the
+ * loading failed.
+ *
+ * @param display the Display object
+ * @param fileName the file name
+ * @param width optional width to create replacement Image. If -1, null be
+ * be returned if the loading fails.
+ * @param height optional height to create replacement Image. If -1, null be
+ * be returned if the loading fails.
+ * @param phColor optional color to create replacement Image. If null, Blue
+ * color will be used.
+ * @return a new Image or null if the loading failed and the optional
+ * replacement size was -1
+ */
+ public Image loadImage(Display display, String fileName, int width, int height,
+ Color phColor) {
+
+ Image img = loadImage(fileName, display);
+
+ if (img == null) {
+ Log.w("ddms", "Couldn't load " + fileName);
+ // if we had the extra parameter to create replacement image then we
+ // create and return it.
+ if (width != -1 && height != -1) {
+ return createPlaceHolderArt(display, width, height,
+ phColor != null ? phColor : display
+ .getSystemColor(SWT.COLOR_BLUE));
+ }
+
+ // otherwise, just return null
+ return null;
+ }
+
+ return img;
+ }
+
+ /**
+ * Create place-holder art with the specified color.
+ */
+ public static Image createPlaceHolderArt(Display display, int width,
+ int height, Color color) {
+ Image img = new Image(display, width, height);
+ GC gc = new GC(img);
+ gc.setForeground(color);
+ gc.drawLine(0, 0, width, height);
+ gc.drawLine(0, height - 1, width, -1);
+ gc.dispose();
+ return img;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java
new file mode 100644
index 0000000..60dc2c0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+/**
+ * Display client info in a two-column format.
+ */
+public class InfoPanel extends TablePanel {
+ private Table mTable;
+ private TableColumn mCol2;
+
+ private static final String mLabels[] = {
+ "DDM-aware?",
+ "App description:",
+ "VM version:",
+ "Process ID:",
+ "Supports Profiling Control:",
+ "Supports HPROF Control:",
+ };
+ private static final int ENT_DDM_AWARE = 0;
+ private static final int ENT_APP_DESCR = 1;
+ private static final int ENT_VM_VERSION = 2;
+ private static final int ENT_PROCESS_ID = 3;
+ private static final int ENT_SUPPORTS_PROFILING = 4;
+ private static final int ENT_SUPPORTS_HPROF = 5;
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mTable.setHeaderVisible(false);
+ mTable.setLinesVisible(false);
+
+ TableColumn col1 = new TableColumn(mTable, SWT.RIGHT);
+ col1.setText("name");
+ mCol2 = new TableColumn(mTable, SWT.LEFT);
+ mCol2.setText("PlaceHolderContentForWidth");
+
+ TableItem item;
+ for (int i = 0; i < mLabels.length; i++) {
+ item = new TableItem(mTable, SWT.NONE);
+ item.setText(0, mLabels[i]);
+ item.setText(1, "-");
+ }
+
+ col1.pack();
+ mCol2.pack();
+
+ return mTable;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTable.setFocus();
+ }
+
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) {
+ if (mTable.isDisposed())
+ return;
+
+ mTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ clientSelected();
+ }
+ });
+ }
+ }
+ }
+
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}
+ */
+ @Override
+ public void clientSelected() {
+ if (mTable.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ if (client == null) {
+ for (int i = 0; i < mLabels.length; i++) {
+ TableItem item = mTable.getItem(i);
+ item.setText(1, "-");
+ }
+ } else {
+ TableItem item;
+ String clientDescription, vmIdentifier, isDdmAware,
+ pid;
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ clientDescription = (cd.getClientDescription() != null) ?
+ cd.getClientDescription() : "?";
+ vmIdentifier = (cd.getVmIdentifier() != null) ?
+ cd.getVmIdentifier() : "?";
+ isDdmAware = cd.isDdmAware() ?
+ "yes" : "no";
+ pid = (cd.getPid() != 0) ?
+ String.valueOf(cd.getPid()) : "?";
+ }
+
+ item = mTable.getItem(ENT_APP_DESCR);
+ item.setText(1, clientDescription);
+ item = mTable.getItem(ENT_VM_VERSION);
+ item.setText(1, vmIdentifier);
+ item = mTable.getItem(ENT_DDM_AWARE);
+ item.setText(1, isDdmAware);
+ item = mTable.getItem(ENT_PROCESS_ID);
+ item.setText(1, pid);
+
+ item = mTable.getItem(ENT_SUPPORTS_PROFILING);
+ if (cd.hasFeature(ClientData.FEATURE_PROFILING_STREAMING)) {
+ item.setText(1, "Yes");
+ } else if (cd.hasFeature(ClientData.FEATURE_PROFILING)) {
+ item.setText(1, "Yes (Application must be able to write on the SD Card)");
+ } else {
+ item.setText(1, "No");
+ }
+
+ item = mTable.getItem(ENT_SUPPORTS_HPROF);
+ if (cd.hasFeature(ClientData.FEATURE_HPROF_STREAMING)) {
+ item.setText(1, "Yes");
+ } else if (cd.hasFeature(ClientData.FEATURE_HPROF)) {
+ item.setText(1, "Yes (Application must be able to write on the SD Card)");
+ } else {
+ item.setText(1, "No");
+ }
+ }
+
+ mCol2.pack();
+
+ //Log.i("ddms", "InfoPanel: changed " + client);
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mTable);
+ }
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java
new file mode 100644
index 0000000..337bff2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java
@@ -0,0 +1,1648 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.annotation.WorkerThread;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Panel with native heap information.
+ */
+public final class NativeHeapPanel extends BaseHeapPanel {
+
+ /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+ * Native+1 at least. We also need 2 more entries for free area and expansion area. */
+ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+ private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+ private static final PaletteData mMapPalette = createPalette();
+
+ private static final int ALLOC_DISPLAY_ALL = 0;
+ private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1;
+ private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2;
+
+ private Display mDisplay;
+
+ private Composite mBase;
+
+ private Label mUpdateStatus;
+
+ /** combo giving choice of what to display: all, pre-zygote, post-zygote */
+ private Combo mAllocDisplayCombo;
+
+ private Button mFullUpdateButton;
+
+ // see CreateControl()
+ //private Button mDiffUpdateButton;
+
+ private Combo mDisplayModeCombo;
+
+ /** stack composite for mode (1-2) & 3 */
+ private Composite mTopStackComposite;
+
+ private StackLayout mTopStackLayout;
+
+ /** stack composite for mode 1 & 2 */
+ private Composite mAllocationStackComposite;
+
+ private StackLayout mAllocationStackLayout;
+
+ /** top level container for mode 1 & 2 */
+ private Composite mTableModeControl;
+
+ /** top level object for the allocation mode */
+ private Control mAllocationModeTop;
+
+ /** top level for the library mode */
+ private Control mLibraryModeTopControl;
+
+ /** composite for page UI and total memory display */
+ private Composite mPageUIComposite;
+
+ private Label mTotalMemoryLabel;
+
+ private Label mPageLabel;
+
+ private Button mPageNextButton;
+
+ private Button mPagePreviousButton;
+
+ private Table mAllocationTable;
+
+ private Table mLibraryTable;
+
+ private Table mLibraryAllocationTable;
+
+ private Table mDetailTable;
+
+ private Label mImage;
+
+ private int mAllocDisplayMode = ALLOC_DISPLAY_ALL;
+
+ /**
+ * pointer to current stackcall thread computation in order to quit it if
+ * required (new update requested)
+ */
+ private StackCallThread mStackCallThread;
+
+ /** Current Library Allocation table fill thread. killed if selection changes */
+ private FillTableThread mFillTableThread;
+
+ /**
+ * current client data. Used to access the malloc info when switching pages
+ * or selecting allocation to show stack call
+ */
+ private ClientData mClientData;
+
+ /**
+ * client data from a previous display. used when asking for an "update & diff"
+ */
+ private ClientData mBackUpClientData;
+
+ /** list of NativeAllocationInfo objects filled with the list from ClientData */
+ private final ArrayList<NativeAllocationInfo> mAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** list of the {@link NativeAllocationInfo} being displayed based on the selection
+ * of {@link #mAllocDisplayCombo}.
+ */
+ private final ArrayList<NativeAllocationInfo> mDisplayedAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */
+ private final ArrayList<NativeAllocationInfo> mBackUpAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** back up of the total memory, used when doing an "update & diff" */
+ private int mBackUpTotalMemory;
+
+ private int mCurrentPage = 0;
+
+ private int mPageCount = 0;
+
+ /**
+ * list of allocation per Library. This is created from the list of
+ * NativeAllocationInfo objects that is stored in the ClientData object. Since we
+ * don't keep this list around, it is recomputed everytime the client
+ * changes.
+ */
+ private final ArrayList<LibraryAllocations> mLibraryAllocations =
+ new ArrayList<LibraryAllocations>();
+
+ /* args to setUpdateStatus() */
+ private static final int NOT_SELECTED = 0;
+
+ private static final int NOT_ENABLED = 1;
+
+ private static final int ENABLED = 2;
+
+ private static final int DISPLAY_PER_PAGE = 20;
+
+ private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$
+ private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$
+ private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$
+ private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$
+ private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$
+
+ /** static formatter object to format all numbers as #,### */
+ private static DecimalFormat sFormatter;
+ static {
+ sFormatter = (DecimalFormat)NumberFormat.getInstance();
+ if (sFormatter == null) {
+ sFormatter = new DecimalFormat("#,###");
+ } else {
+ sFormatter.applyPattern("#,###");
+ }
+ }
+
+
+ /**
+ * caching mechanism to avoid recomputing the backtrace for a particular
+ * address several times.
+ */
+ private HashMap<Long, NativeStackCallInfo> mSourceCache =
+ new HashMap<Long, NativeStackCallInfo>();
+ private long mTotalSize;
+ private Button mSaveButton;
+ private Button mSymbolsButton;
+
+ /**
+ * thread class to convert the address call into method, file and line
+ * number in the background.
+ */
+ private class StackCallThread extends BackgroundThread {
+ private ClientData mClientData;
+
+ public StackCallThread(ClientData cd) {
+ mClientData = cd;
+ }
+
+ public ClientData getClientData() {
+ return mClientData;
+ }
+
+ @Override
+ public void run() {
+ // loop through all the NativeAllocationInfo and init them
+ Iterator<NativeAllocationInfo> iter = mAllocations.iterator();
+ int total = mAllocations.size();
+ int count = 0;
+ while (iter.hasNext()) {
+
+ if (isQuitting())
+ return;
+
+ NativeAllocationInfo info = iter.next();
+ if (info.isStackCallResolved() == false) {
+ final List<Long> list = info.getStackCallAddresses();
+ final int size = list.size();
+
+ ArrayList<NativeStackCallInfo> resolvedStackCall =
+ new ArrayList<NativeStackCallInfo>();
+
+ for (int i = 0; i < size; i++) {
+ long addr = list.get(i);
+
+ // first check if the addr has already been converted.
+ NativeStackCallInfo source = mSourceCache.get(addr);
+
+ // if not we convert it
+ if (source == null) {
+ source = sourceForAddr(addr);
+ mSourceCache.put(addr, source);
+ }
+
+ resolvedStackCall.add(source);
+ }
+
+ info.setResolvedStackCall(resolvedStackCall);
+ }
+ // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless
+ // we reach total, since we also do it after the loop
+ // (only an issue in case we have a perfect number of page)
+ count++;
+ if ((count % DISPLAY_PER_PAGE) == 0 && count != total) {
+ if (updateNHAllocationStackCalls(mClientData, count) == false) {
+ // looks like the app is quitting, so we just
+ // stopped the thread
+ return;
+ }
+ }
+ }
+
+ updateNHAllocationStackCalls(mClientData, count);
+ }
+
+ private NativeStackCallInfo sourceForAddr(long addr) {
+ NativeLibraryMapInfo library = getLibraryFor(addr);
+
+ if (library != null) {
+
+ Addr2Line process = Addr2Line.getProcess(library);
+ if (process != null) {
+ // remove the base of the library address
+ NativeStackCallInfo info = process.getAddress(addr);
+ if (info != null) {
+ return info;
+ }
+ }
+ }
+
+ return new NativeStackCallInfo(addr,
+ library != null ? library.getLibraryName() : null,
+ Long.toHexString(addr),
+ "");
+ }
+
+ private NativeLibraryMapInfo getLibraryFor(long addr) {
+ for (NativeLibraryMapInfo info : mClientData.getMappedNativeLibraries()) {
+ if (info.isWithinLibrary(addr)) {
+ return info;
+ }
+ }
+
+ Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
+ return null;
+ }
+
+ /**
+ * update the Native Heap panel with the amount of allocation for which the
+ * stack call has been computed. This is called from a non UI thread, but
+ * will be executed in the UI thread.
+ *
+ * @param count the amount of allocation
+ * @return false if the display was disposed and the update couldn't happen
+ */
+ private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) {
+ if (mDisplay.isDisposed() == false) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateAllocationStackCalls(clientData, count);
+ }
+ });
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private class FillTableThread extends BackgroundThread {
+ private LibraryAllocations mLibAlloc;
+
+ private int mMax;
+
+ public FillTableThread(LibraryAllocations liballoc, int m) {
+ mLibAlloc = liballoc;
+ mMax = m;
+ }
+
+ @Override
+ public void run() {
+ for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) {
+ updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10);
+ }
+ }
+
+ /**
+ * updates the library allocation table in the Native Heap panel. This is
+ * called from a non UI thread, but will be executed in the UI thread.
+ *
+ * @param liballoc the current library allocation object being displayed
+ * @param start start index of items that need to be displayed
+ * @param end end index of the items that need to be displayed
+ */
+ private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc,
+ final int start, final int end) {
+ if (mDisplay.isDisposed() == false) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateLibraryAllocationTable(libAlloc, start, end);
+ }
+ });
+ }
+
+ }
+ }
+
+ /** class to aggregate allocations per library */
+ public static class LibraryAllocations {
+ private String mLibrary;
+
+ private final ArrayList<NativeAllocationInfo> mLibAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ private int mSize;
+
+ private int mCount;
+
+ /** construct the aggregate object for a library */
+ public LibraryAllocations(final String lib) {
+ mLibrary = lib;
+ }
+
+ /** get the library name */
+ public String getLibrary() {
+ return mLibrary;
+ }
+
+ /** add a NativeAllocationInfo object to this aggregate object */
+ public void addAllocation(NativeAllocationInfo info) {
+ mLibAllocations.add(info);
+ }
+
+ /** get an iterator on the NativeAllocationInfo objects */
+ public Iterator<NativeAllocationInfo> getAllocations() {
+ return mLibAllocations.iterator();
+ }
+
+ /** get a NativeAllocationInfo object by index */
+ public NativeAllocationInfo getAllocation(int index) {
+ return mLibAllocations.get(index);
+ }
+
+ /** returns the NativeAllocationInfo object count */
+ public int getAllocationSize() {
+ return mLibAllocations.size();
+ }
+
+ /** returns the total allocation size */
+ public int getSize() {
+ return mSize;
+ }
+
+ /** returns the number of allocations */
+ public int getCount() {
+ return mCount;
+ }
+
+ /**
+ * compute the allocation count and size for allocation objects added
+ * through <code>addAllocation()</code>, and sort the objects by
+ * total allocation size.
+ */
+ public void computeAllocationSizeAndCount() {
+ mSize = 0;
+ mCount = 0;
+ for (NativeAllocationInfo info : mLibAllocations) {
+ mCount += info.getAllocationCount();
+ mSize += info.getAllocationCount() * info.getSize();
+ }
+ Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() {
+ @Override
+ public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) {
+ return o2.getAllocationCount() * o2.getSize() -
+ o1.getAllocationCount() * o1.getSize();
+ }
+ });
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+
+ mDisplay = parent.getDisplay();
+
+ mBase = new Composite(parent, SWT.NONE);
+ GridLayout gl = new GridLayout(1, false);
+ gl.horizontalSpacing = 0;
+ gl.verticalSpacing = 0;
+ mBase.setLayout(gl);
+ mBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // composite for <update btn> <status>
+ Composite tmp = new Composite(mBase, SWT.NONE);
+ tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ tmp.setLayout(gl = new GridLayout(2, false));
+ gl.marginWidth = gl.marginHeight = 0;
+
+ mFullUpdateButton = new Button(tmp, SWT.NONE);
+ mFullUpdateButton.setText("Full Update");
+ mFullUpdateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mBackUpClientData = null;
+ mDisplayModeCombo.setEnabled(false);
+ mSaveButton.setEnabled(false);
+ emptyTables();
+ // if we already have a stack call computation for this
+ // client
+ // we stop it
+ if (mStackCallThread != null &&
+ mStackCallThread.getClientData() == mClientData) {
+ mStackCallThread.quit();
+ mStackCallThread = null;
+ }
+ mLibraryAllocations.clear();
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestNativeHeapInformation();
+ }
+ }
+ });
+
+ mUpdateStatus = new Label(tmp, SWT.NONE);
+ mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // top layout for the combos and oter controls on the right.
+ Composite top_layout = new Composite(mBase, SWT.NONE);
+ top_layout.setLayout(gl = new GridLayout(4, false));
+ gl.marginWidth = gl.marginHeight = 0;
+
+ new Label(top_layout, SWT.NONE).setText("Show:");
+
+ mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mAllocDisplayCombo.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mAllocDisplayCombo.add("All Allocations");
+ mAllocDisplayCombo.add("Pre-Zygote Allocations");
+ mAllocDisplayCombo.add("Zygote Child Allocations (Z)");
+ mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onAllocDisplayChange();
+ }
+ });
+ mAllocDisplayCombo.select(0);
+
+ // separator
+ Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL);
+ GridData gd;
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+ gd.verticalSpan = 2;
+
+ mSaveButton = new Button(top_layout, SWT.PUSH);
+ mSaveButton.setText("Save...");
+ mSaveButton.setEnabled(false);
+ mSaveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Save Allocations");
+ fileDialog.setFileName("allocations.txt");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ saveAllocations(fileName);
+ }
+ }
+ });
+
+ /*
+ * TODO: either fix the diff mechanism or remove it altogether.
+ mDiffUpdateButton = new Button(top_layout, SWT.NONE);
+ mDiffUpdateButton.setText("Update && Diff");
+ mDiffUpdateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // since this is an update and diff, we need to store the
+ // current list
+ // of mallocs
+ mBackUpAllocations.clear();
+ mBackUpAllocations.addAll(mAllocations);
+ mBackUpClientData = mClientData;
+ mBackUpTotalMemory = mClientData.getTotalNativeMemory();
+
+ mDisplayModeCombo.setEnabled(false);
+ emptyTables();
+ // if we already have a stack call computation for this
+ // client
+ // we stop it
+ if (mStackCallThread != null &&
+ mStackCallThread.getClientData() == mClientData) {
+ mStackCallThread.quit();
+ mStackCallThread = null;
+ }
+ mLibraryAllocations.clear();
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestNativeHeapInformation();
+ }
+ }
+ });
+ */
+
+ Label l = new Label(top_layout, SWT.NONE);
+ l.setText("Display:");
+
+ mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mDisplayModeCombo.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" });
+ mDisplayModeCombo.select(0);
+ mDisplayModeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switchDisplayMode();
+ }
+ });
+ mDisplayModeCombo.setEnabled(false);
+
+ mSymbolsButton = new Button(top_layout, SWT.PUSH);
+ mSymbolsButton.setText("Load Symbols");
+ mSymbolsButton.setEnabled(false);
+
+
+ // create a composite that will contains the actual content composites,
+ // in stack mode layout.
+ // This top level composite contains 2 other composites.
+ // * one for both Allocations and Libraries mode
+ // * one for flat mode (which is gone for now)
+
+ mTopStackComposite = new Composite(mBase, SWT.NONE);
+ mTopStackComposite.setLayout(mTopStackLayout = new StackLayout());
+ mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // create 1st and 2nd modes
+ createTableDisplay(mTopStackComposite);
+
+ mTopStackLayout.topControl = mTableModeControl;
+ mTopStackComposite.layout();
+
+ setUpdateStatus(NOT_SELECTED);
+
+ // Work in progress
+ // TODO add image display of native heap.
+ //mImage = new Label(mBase, SWT.NONE);
+
+ mBase.pack();
+
+ return mBase;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ // TODO
+ }
+
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) {
+ if (mBase.isDisposed())
+ return;
+
+ mBase.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ clientSelected();
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mBase.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ mDisplayModeCombo.setEnabled(false);
+ emptyTables();
+
+ Log.d("ddms", "NativeHeapPanel: changed " + client);
+
+ if (client != null) {
+ ClientData cd = client.getClientData();
+ mClientData = cd;
+
+ // if (cd.getShowHeapUpdates())
+ setUpdateStatus(ENABLED);
+ // else
+ // setUpdateStatus(NOT_ENABLED);
+
+ initAllocationDisplay();
+
+ //renderBitmap(cd);
+ } else {
+ mClientData = null;
+ setUpdateStatus(NOT_SELECTED);
+ }
+
+ mBase.pack();
+ }
+
+ /**
+ * Update the UI with the newly compute stack calls, unless the UI switched
+ * to a different client.
+ *
+ * @param cd the ClientData for which the stack call are being computed.
+ * @param count the current count of allocations for which the stack calls
+ * have been computed.
+ */
+ @WorkerThread
+ public void updateAllocationStackCalls(ClientData cd, int count) {
+ // we have to check that the panel still shows the same clientdata than
+ // the thread is computing for.
+ if (cd == mClientData) {
+
+ int total = mAllocations.size();
+
+ if (count == total) {
+ // we're done: do something
+ mDisplayModeCombo.setEnabled(true);
+ mSaveButton.setEnabled(true);
+
+ mStackCallThread = null;
+ } else {
+ // work in progress, update the progress bar.
+// mUiThread.setStatusLine("Computing stack call: " + count
+// + "/" + total);
+ }
+
+ // FIXME: attempt to only update when needed.
+ // Because the number of pages is not related to mAllocations.size() anymore
+ // due to pre-zygote/post-zygote display, update all the time.
+ // At some point we should remove the pages anyway, since it's getting computed
+ // really fast now.
+// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count
+// || (count == total && mCurrentPage == mPageCount - 1)) {
+ try {
+ // get the current selection of the allocation
+ int index = mAllocationTable.getSelectionIndex();
+ NativeAllocationInfo info = null;
+
+ if (index != -1) {
+ info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData();
+ }
+
+ // empty the table
+ emptyTables();
+
+ // fill it again
+ fillAllocationTable();
+
+ // reselect
+ mAllocationTable.setSelection(index);
+
+ // display detail table if needed
+ if (info != null) {
+ fillDetailTable(info);
+ }
+ } catch (SWTException e) {
+ if (mAllocationTable.isDisposed()) {
+ // looks like the table is disposed. Let's ignore it.
+ } else {
+ throw e;
+ }
+ }
+
+ } else {
+ // old client still running. doesn't really matter.
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mAllocationTable);
+ addTableToFocusListener(mLibraryTable);
+ addTableToFocusListener(mLibraryAllocationTable);
+ addTableToFocusListener(mDetailTable);
+ }
+
+ protected void onAllocDisplayChange() {
+ mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex();
+
+ // create the new list
+ updateAllocDisplayList();
+
+ updateTotalMemoryDisplay();
+
+ // reset the ui.
+ mCurrentPage = 0;
+ updatePageUI();
+ switchDisplayMode();
+ }
+
+ private void updateAllocDisplayList() {
+ mTotalSize = 0;
+ mDisplayedAllocations.clear();
+ for (NativeAllocationInfo info : mAllocations) {
+ if (mAllocDisplayMode == ALLOC_DISPLAY_ALL ||
+ (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) {
+ mDisplayedAllocations.add(info);
+ mTotalSize += info.getSize() * info.getAllocationCount();
+ } else {
+ // skip this item
+ continue;
+ }
+ }
+
+ int count = mDisplayedAllocations.size();
+
+ mPageCount = count / DISPLAY_PER_PAGE;
+
+ // need to add a page for the rest of the div
+ if ((count % DISPLAY_PER_PAGE) > 0) {
+ mPageCount++;
+ }
+ }
+
+ private void updateTotalMemoryDisplay() {
+ switch (mAllocDisplayMode) {
+ case ALLOC_DISPLAY_ALL:
+ mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ case ALLOC_DISPLAY_PRE_ZYGOTE:
+ mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ case ALLOC_DISPLAY_POST_ZYGOTE:
+ mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ }
+ }
+
+
+ private void switchDisplayMode() {
+ switch (mDisplayModeCombo.getSelectionIndex()) {
+ case 0: {// allocations
+ mTopStackLayout.topControl = mTableModeControl;
+ mAllocationStackLayout.topControl = mAllocationModeTop;
+ mAllocationStackComposite.layout();
+ mTopStackComposite.layout();
+ emptyTables();
+ fillAllocationTable();
+ }
+ break;
+ case 1: {// libraries
+ mTopStackLayout.topControl = mTableModeControl;
+ mAllocationStackLayout.topControl = mLibraryModeTopControl;
+ mAllocationStackComposite.layout();
+ mTopStackComposite.layout();
+ emptyTables();
+ fillLibraryTable();
+ }
+ break;
+ }
+ }
+
+ private void initAllocationDisplay() {
+ if (mStackCallThread != null) {
+ mStackCallThread.quit();
+ }
+
+ mAllocations.clear();
+ mAllocations.addAll(mClientData.getNativeAllocationList());
+
+ updateAllocDisplayList();
+
+ // if we have a previous clientdata and it matches the current one. we
+ // do a diff between the new list and the old one.
+ if (mBackUpClientData != null && mBackUpClientData == mClientData) {
+
+ ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>();
+
+ // we go through the list of NativeAllocationInfo in the new list and check if
+ // there's one with the same exact data (size, allocation, count and
+ // stackcall addresses) in the old list.
+ // if we don't find any, we add it to the "add" list
+ for (NativeAllocationInfo mi : mAllocations) {
+ boolean found = false;
+ for (NativeAllocationInfo old_mi : mBackUpAllocations) {
+ if (mi.equals(old_mi)) {
+ found = true;
+ break;
+ }
+ }
+ if (found == false) {
+ add.add(mi);
+ }
+ }
+
+ // put the result in mAllocations
+ mAllocations.clear();
+ mAllocations.addAll(add);
+
+ // display the difference in memory usage. This is computed
+ // calculating the memory usage of the objects in mAllocations.
+ int count = 0;
+ for (NativeAllocationInfo allocInfo : mAllocations) {
+ count += allocInfo.getSize() * allocInfo.getAllocationCount();
+ }
+
+ mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes",
+ sFormatter.format(count)));
+ }
+ else {
+ // display the full memory usage
+ updateTotalMemoryDisplay();
+ //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0);
+ }
+ mTotalMemoryLabel.pack();
+
+ // update the page ui
+ mDisplayModeCombo.select(0);
+
+ mLibraryAllocations.clear();
+
+ // reset to first page
+ mCurrentPage = 0;
+
+ // update the label
+ updatePageUI();
+
+ // now fill the allocation Table with the current page
+ switchDisplayMode();
+
+ // start the thread to compute the stack calls
+ if (mAllocations.size() > 0) {
+ mStackCallThread = new StackCallThread(mClientData);
+ mStackCallThread.start();
+ }
+ }
+
+ private void updatePageUI() {
+
+ // set the label and pack to update the layout, otherwise
+ // the label will be cut off if the new size is bigger
+ if (mPageCount == 0) {
+ mPageLabel.setText("0 of 0 allocations.");
+ } else {
+ StringBuffer buffer = new StringBuffer();
+ // get our starting index
+ int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1;
+ // end index, taking into account the last page can be half full
+ int count = mDisplayedAllocations.size();
+ int end = Math.min(start + DISPLAY_PER_PAGE - 1, count);
+ buffer.append(sFormatter.format(start));
+ buffer.append(" - ");
+ buffer.append(sFormatter.format(end));
+ buffer.append(" of ");
+ buffer.append(sFormatter.format(count));
+ buffer.append(" allocations.");
+ mPageLabel.setText(buffer.toString());
+ }
+
+ // handle the button enabled state.
+ mPagePreviousButton.setEnabled(mCurrentPage > 0);
+ // reminder: mCurrentPage starts at 0.
+ mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1);
+
+ mPageLabel.pack();
+ mPageUIComposite.pack();
+
+ }
+
+ private void fillAllocationTable() {
+ // get the count
+ int count = mDisplayedAllocations.size();
+
+ // get our starting index
+ int start = mCurrentPage * DISPLAY_PER_PAGE;
+
+ // loop for DISPLAY_PER_PAGE or till we reach count
+ int end = start + DISPLAY_PER_PAGE;
+
+ for (int i = start; i < end && i < count; i++) {
+ NativeAllocationInfo info = mDisplayedAllocations.get(i);
+
+ TableItem item = null;
+
+ if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) {
+ item = new TableItem(mAllocationTable, SWT.NONE);
+ item.setText(0, (info.isZygoteChild() ? "Z " : "") +
+ sFormatter.format(info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+ } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) {
+ item = new TableItem(mAllocationTable, SWT.NONE);
+ item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+ } else {
+ // skip this item
+ continue;
+ }
+
+ item.setData(info);
+
+ NativeStackCallInfo bti = info.getRelevantStackCallInfo();
+ if (bti != null) {
+ String lib = bti.getLibraryName();
+ String method = bti.getMethodName();
+ String source = bti.getSourceFile();
+ if (lib != null)
+ item.setText(3, lib);
+ if (method != null)
+ item.setText(4, method);
+ if (source != null)
+ item.setText(5, source);
+ }
+ }
+ }
+
+ private void fillLibraryTable() {
+ // fill the library table
+ sortAllocationsPerLibrary();
+
+ for (LibraryAllocations liballoc : mLibraryAllocations) {
+ if (liballoc != null) {
+ TableItem item = new TableItem(mLibraryTable, SWT.NONE);
+ String lib = liballoc.getLibrary();
+ item.setText(0, lib != null ? lib : "");
+ item.setText(1, sFormatter.format(liballoc.getSize()));
+ item.setText(2, sFormatter.format(liballoc.getCount()));
+ }
+ }
+ }
+
+ private void fillLibraryAllocationTable() {
+ mLibraryAllocationTable.removeAll();
+ mDetailTable.removeAll();
+ int index = mLibraryTable.getSelectionIndex();
+ if (index != -1) {
+ LibraryAllocations liballoc = mLibraryAllocations.get(index);
+ // start a thread that will fill table 10 at a time to keep the ui
+ // responsive, but first we kill the previous one if there was one
+ if (mFillTableThread != null) {
+ mFillTableThread.quit();
+ }
+ mFillTableThread = new FillTableThread(liballoc,
+ liballoc.getAllocationSize());
+ mFillTableThread.start();
+ }
+ }
+
+ public void updateLibraryAllocationTable(LibraryAllocations liballoc,
+ int start, int end) {
+ try {
+ if (mLibraryTable.isDisposed() == false) {
+ int index = mLibraryTable.getSelectionIndex();
+ if (index != -1) {
+ LibraryAllocations newliballoc = mLibraryAllocations.get(
+ index);
+ if (newliballoc == liballoc) {
+ int count = liballoc.getAllocationSize();
+ for (int i = start; i < end && i < count; i++) {
+ NativeAllocationInfo info = liballoc.getAllocation(i);
+
+ TableItem item = new TableItem(
+ mLibraryAllocationTable, SWT.NONE);
+ item.setText(0, sFormatter.format(
+ info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+
+ NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo();
+ if (stackCallInfo != null) {
+ item.setText(3, stackCallInfo.getMethodName());
+ }
+ }
+ } else {
+ // we should quit the thread
+ if (mFillTableThread != null) {
+ mFillTableThread.quit();
+ mFillTableThread = null;
+ }
+ }
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("ddms", "error when updating the library allocation table");
+ }
+ }
+
+ private void fillDetailTable(final NativeAllocationInfo mi) {
+ mDetailTable.removeAll();
+ mDetailTable.setRedraw(false);
+
+ try {
+ // populate the detail Table with the back trace
+ List<Long> addresses = mi.getStackCallAddresses();
+ List<NativeStackCallInfo> resolvedStackCall = mi.getResolvedStackCall();
+
+ if (resolvedStackCall == null) {
+ return;
+ }
+
+ for (int i = 0 ; i < resolvedStackCall.size(); i++) {
+ if (addresses.get(i) == null || addresses.get(i).longValue() == 0) {
+ continue;
+ }
+
+ long addr = addresses.get(i).longValue();
+ NativeStackCallInfo source = resolvedStackCall.get(i);
+
+ TableItem item = new TableItem(mDetailTable, SWT.NONE);
+ item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$
+
+ String libraryName = source.getLibraryName();
+ String methodName = source.getMethodName();
+ String sourceFile = source.getSourceFile();
+ int lineNumber = source.getLineNumber();
+
+ if (libraryName != null)
+ item.setText(1, libraryName);
+ if (methodName != null)
+ item.setText(2, methodName);
+ if (sourceFile != null)
+ item.setText(3, sourceFile);
+ if (lineNumber != -1)
+ item.setText(4, Integer.toString(lineNumber));
+ }
+ } finally {
+ mDetailTable.setRedraw(true);
+ }
+ }
+
+ /*
+ * Are updates enabled?
+ */
+ private void setUpdateStatus(int status) {
+ switch (status) {
+ case NOT_SELECTED:
+ mUpdateStatus.setText("Select a client to see heap info");
+ mAllocDisplayCombo.setEnabled(false);
+ mFullUpdateButton.setEnabled(false);
+ //mDiffUpdateButton.setEnabled(false);
+ break;
+ case NOT_ENABLED:
+ mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client");
+ mAllocDisplayCombo.setEnabled(false);
+ mFullUpdateButton.setEnabled(false);
+ //mDiffUpdateButton.setEnabled(false);
+ break;
+ case ENABLED:
+ mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data");
+ mAllocDisplayCombo.setEnabled(true);
+ mFullUpdateButton.setEnabled(true);
+ //mDiffUpdateButton.setEnabled(true);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
+ mUpdateStatus.pack();
+ }
+
+ /**
+ * Create the Table display. This includes a "detail" Table in the bottom
+ * half and 2 modes in the top half: allocation Table and
+ * library+allocations Tables.
+ *
+ * @param base the top parent to create the display into
+ */
+ private void createTableDisplay(Composite base) {
+ final int minPanelWidth = 60;
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ // top level composite for mode 1 & 2
+ mTableModeControl = new Composite(base, SWT.NONE);
+ GridLayout gl = new GridLayout(1, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ mTableModeControl.setLayout(gl);
+ mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE);
+ mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTotalMemoryLabel.setText("Total Memory: 0 Bytes");
+
+ // the top half of these modes is dynamic
+
+ final Composite sash_composite = new Composite(mTableModeControl,
+ SWT.NONE);
+ sash_composite.setLayout(new FormLayout());
+ sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // create the stacked composite
+ mAllocationStackComposite = new Composite(sash_composite, SWT.NONE);
+ mAllocationStackLayout = new StackLayout();
+ mAllocationStackComposite.setLayout(mAllocationStackLayout);
+ mAllocationStackComposite.setLayoutData(new GridData(
+ GridData.FILL_BOTH));
+
+ // create the top half for mode 1
+ createAllocationTopHalf(mAllocationStackComposite);
+
+ // create the top half for mode 2
+ createLibraryTopHalf(mAllocationStackComposite);
+
+ final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL);
+
+ // bottom half of these modes is the same: detail table
+ createDetailTable(sash_composite);
+
+ // init value for stack
+ mAllocationStackLayout.topControl = mAllocationModeTop;
+
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(mTotalMemoryLabel, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mAllocationStackComposite.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) {
+ sashData.top = new FormAttachment(0,
+ prefs.getInt(PREFS_ALLOCATION_SASH));
+ } else {
+ sashData.top = new FormAttachment(50, 0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mDetailTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = sash_composite.getClientArea();
+ int bottom = panelRect.height - sashRect.height - minPanelWidth;
+ e.y = Math.max(Math.min(e.y, bottom), minPanelWidth);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ prefs.setValue(PREFS_ALLOCATION_SASH, e.y);
+ sash_composite.layout();
+ }
+ }
+ });
+ }
+
+ private void createDetailTable(Composite base) {
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+ mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mDetailTable.setHeaderVisible(true);
+ mDetailTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT,
+ "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT,
+ "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$
+ }
+
+ private void createAllocationTopHalf(Composite b) {
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ Composite base = new Composite(b, SWT.NONE);
+ mAllocationModeTop = base;
+ GridLayout gl = new GridLayout(1, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ gl.verticalSpacing = 0;
+ base.setLayout(gl);
+ base.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // horizontal layout for memory total and pages UI
+ mPageUIComposite = new Composite(base, SWT.NONE);
+ mPageUIComposite.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_BEGINNING));
+ gl = new GridLayout(3, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ gl.horizontalSpacing = 0;
+ mPageUIComposite.setLayout(gl);
+
+ // Page UI
+ mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE);
+ mPagePreviousButton.setText("<");
+ mPagePreviousButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mCurrentPage--;
+ updatePageUI();
+ emptyTables();
+ fillAllocationTable();
+ }
+ });
+
+ mPageNextButton = new Button(mPageUIComposite, SWT.NONE);
+ mPageNextButton.setText(">");
+ mPageNextButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mCurrentPage++;
+ updatePageUI();
+ emptyTables();
+ fillAllocationTable();
+ }
+ });
+
+ mPageLabel = new Label(mPageUIComposite, SWT.NONE);
+ mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ updatePageUI();
+
+ mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+ mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mAllocationTable.setHeaderVisible(true);
+ mAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT,
+ "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT,
+ "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT,
+ "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$
+
+ mAllocationTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the selection index
+ int index = mAllocationTable.getSelectionIndex();
+ if (index >= 0 && index < mAllocationTable.getItemCount()) {
+ TableItem item = mAllocationTable.getItem(index);
+ if (item != null && item.getData() instanceof NativeAllocationInfo) {
+ fillDetailTable((NativeAllocationInfo)item.getData());
+ }
+ }
+ }
+ });
+ }
+
+ private void createLibraryTopHalf(Composite base) {
+ final int minPanelWidth = 60;
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ // create a composite that'll contain 2 tables horizontally
+ final Composite top = new Composite(base, SWT.NONE);
+ mLibraryModeTopControl = top;
+ top.setLayout(new FormLayout());
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // first table: library
+ mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+ mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mLibraryTable.setHeaderVisible(true);
+ mLibraryTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT,
+ "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT,
+ "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$
+
+ mLibraryTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ fillLibraryAllocationTable();
+ }
+ });
+
+ final Sash sash = new Sash(top, SWT.VERTICAL);
+
+ // 2nd table: allocation per library
+ mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+ mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mLibraryAllocationTable.setHeaderVisible(true);
+ mLibraryAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Total",
+ SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Count",
+ SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Size",
+ SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Method",
+ SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$
+
+ mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the index of the selection in the library table
+ int index1 = mLibraryTable.getSelectionIndex();
+ // get the index in the library allocation table
+ int index2 = mLibraryAllocationTable.getSelectionIndex();
+ // get the MallocInfo object
+ if (index1 != -1 && index2 != -1) {
+ LibraryAllocations liballoc = mLibraryAllocations.get(index1);
+ NativeAllocationInfo info = liballoc.getAllocation(index2);
+ fillDetailTable(info);
+ }
+ }
+ });
+
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(sash, 0);
+ mLibraryTable.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) {
+ sashData.left = new FormAttachment(0,
+ prefs.getInt(PREFS_LIBRARY_SASH));
+ } else {
+ sashData.left = new FormAttachment(50, 0);
+ }
+ sashData.bottom = new FormAttachment(100, 0);
+ sashData.top = new FormAttachment(0, 0); // 50% across
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(sash, 0);
+ data.right = new FormAttachment(100, 0);
+ mLibraryAllocationTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = top.getClientArea();
+ int right = panelRect.width - sashRect.width - minPanelWidth;
+ e.x = Math.max(Math.min(e.x, right), minPanelWidth);
+ if (e.x != sashRect.x) {
+ sashData.left = new FormAttachment(0, e.x);
+ prefs.setValue(PREFS_LIBRARY_SASH, e.y);
+ top.layout();
+ }
+ }
+ });
+ }
+
+ private void emptyTables() {
+ mAllocationTable.removeAll();
+ mLibraryTable.removeAll();
+ mLibraryAllocationTable.removeAll();
+ mDetailTable.removeAll();
+ }
+
+ private void sortAllocationsPerLibrary() {
+ if (mClientData != null) {
+ mLibraryAllocations.clear();
+
+ // create a hash map of LibraryAllocations to access aggregate
+ // objects already created
+ HashMap<String, LibraryAllocations> libcache =
+ new HashMap<String, LibraryAllocations>();
+
+ // get the allocation count
+ int count = mDisplayedAllocations.size();
+ for (int i = 0; i < count; i++) {
+ NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i);
+
+ NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo();
+ if (stackCallInfo != null) {
+ String libraryName = stackCallInfo.getLibraryName();
+ LibraryAllocations liballoc = libcache.get(libraryName);
+ if (liballoc == null) {
+ // didn't find a library allocation object already
+ // created so we create one
+ liballoc = new LibraryAllocations(libraryName);
+ // add it to the cache
+ libcache.put(libraryName, liballoc);
+ // add it to the list
+ mLibraryAllocations.add(liballoc);
+ }
+ // add the MallocInfo object to it.
+ liballoc.addAllocation(allocInfo);
+ }
+ }
+ // now that the list is created, we need to compute the size and
+ // sort it by size. This will also sort the MallocInfo objects
+ // inside each LibraryAllocation objects.
+ for (LibraryAllocations liballoc : mLibraryAllocations) {
+ liballoc.computeAllocationSizeAndCount();
+ }
+
+ // now we sort it
+ Collections.sort(mLibraryAllocations,
+ new Comparator<LibraryAllocations>() {
+ @Override
+ public int compare(LibraryAllocations o1,
+ LibraryAllocations o2) {
+ return o2.getSize() - o1.getSize();
+ }
+ });
+ }
+ }
+
+ private void renderBitmap(ClientData cd) {
+ byte[] pixData;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false) {
+ // no change, we return.
+ return;
+ }
+
+ pixData = getSerializedData();
+
+ ImageData id = createLinearHeapImage(pixData, 200, mMapPalette);
+ Image image = new Image(mBase.getDisplay(), id);
+ mImage.setImage(image);
+ mImage.pack(true);
+ }
+ }
+
+ /*
+ * Create color palette for map. Set up titles for legend.
+ */
+ private static PaletteData createPalette() {
+ RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+ colors[0]
+ = new RGB(192, 192, 192); // non-heap pixels are gray
+ mMapLegend[0]
+ = "(heap expansion area)";
+
+ colors[1]
+ = new RGB(0, 0, 0); // free chunks are black
+ mMapLegend[1]
+ = "free";
+
+ colors[HeapSegmentElement.KIND_OBJECT + 2]
+ = new RGB(0, 0, 255); // objects are blue
+ mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+ = "data object";
+
+ colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = new RGB(0, 255, 0); // class objects are green
+ mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = "class object";
+
+ colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = new RGB(255, 0, 0); // byte/bool arrays are red
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = "1-byte array (byte[], boolean[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = new RGB(255, 128, 0); // short/char arrays are orange
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = "2-byte array (short[], char[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = new RGB(255, 255, 0); // obj/int/float arrays are yellow
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = "4-byte array (object[], int[], float[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = new RGB(255, 128, 128); // long/double arrays are pink
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = "8-byte array (long[], double[])";
+
+ colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = new RGB(255, 0, 255); // unknown objects are cyan
+ mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = "unknown object";
+
+ colors[HeapSegmentElement.KIND_NATIVE + 2]
+ = new RGB(64, 64, 64); // native objects are dark gray
+ mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+ = "non-Java object";
+
+ return new PaletteData(colors);
+ }
+
+ private void saveAllocations(String fileName) {
+ try {
+ PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
+
+ for (NativeAllocationInfo alloc : mAllocations) {
+ out.println(alloc.toString());
+ }
+ out.close();
+ } catch (IOException e) {
+ Log.e("Native", e);
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java
new file mode 100644
index 0000000..d910cc7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+
+/**
+ * Base class for our information panels.
+ */
+public abstract class Panel {
+
+ public final Control createPanel(Composite parent) {
+ Control panelControl = createControl(parent);
+
+ postCreation();
+
+ return panelControl;
+ }
+
+ protected abstract void postCreation();
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ protected abstract Control createControl(Composite parent);
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ public abstract void setFocus();
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java
new file mode 100644
index 0000000..533372e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Edit an integer field, validating it as a port number.
+ */
+public class PortFieldEditor extends IntegerFieldEditor {
+
+ public boolean mRecursiveCheck = false;
+
+ public PortFieldEditor(String name, String label, Composite parent) {
+ super(name, label, parent);
+ setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+ }
+
+ /*
+ * Get the current value of the field, as an integer.
+ */
+ public int getCurrentValue() {
+ int val;
+ try {
+ val = Integer.parseInt(getStringValue());
+ }
+ catch (NumberFormatException nfe) {
+ val = -1;
+ }
+ return val;
+ }
+
+ /*
+ * Check the validity of the field.
+ */
+ @Override
+ protected boolean checkState() {
+ if (super.checkState() == false) {
+ return false;
+ }
+ //Log.i("ddms", "check state " + getStringValue());
+ boolean err = false;
+ int val = getCurrentValue();
+ if (val < 1024 || val > 32767) {
+ setErrorMessage("Port must be between 1024 and 32767");
+ err = true;
+ } else {
+ setErrorMessage(null);
+ err = false;
+ }
+ showErrorMessage();
+ return !err;
+ }
+
+ protected void updateCheckState(PortFieldEditor pfe) {
+ pfe.refreshValidState();
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java
new file mode 100644
index 0000000..b0f885a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.ImageTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+
+
+/**
+ * Gather a screen shot from the device and save it to a file.
+ */
+public class ScreenShotDialog extends Dialog {
+
+ private Label mBusyLabel;
+ private Label mImageLabel;
+ private Button mSave;
+ private IDevice mDevice;
+ private RawImage mRawImage;
+ private Clipboard mClipboard;
+
+ /** Number of 90 degree rotations applied to the current image */
+ private int mRotateCount = 0;
+
+ /**
+ * Create with default style.
+ */
+ public ScreenShotDialog(Shell parent) {
+ this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+ mClipboard = new Clipboard(parent.getDisplay());
+ }
+
+ /**
+ * Create with app-defined style.
+ */
+ public ScreenShotDialog(Shell parent, int style) {
+ super(parent, style);
+ }
+
+ /**
+ * Prepare and display the dialog.
+ * @param device The {@link IDevice} from which to get the screenshot.
+ */
+ public void open(IDevice device) {
+ mDevice = device;
+
+ Shell parent = getParent();
+ Shell shell = new Shell(parent, getStyle());
+ shell.setText("Device Screen Capture");
+
+ createContents(shell);
+ shell.pack();
+ shell.open();
+
+ updateDeviceImage(shell);
+
+ Display display = parent.getDisplay();
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ }
+
+ /*
+ * Create the screen capture dialog contents.
+ */
+ private void createContents(final Shell shell) {
+ GridData data;
+
+ final int colCount = 5;
+
+ shell.setLayout(new GridLayout(colCount, true));
+
+ // "refresh" button
+ Button refresh = new Button(shell, SWT.PUSH);
+ refresh.setText("Refresh");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ refresh.setLayoutData(data);
+ refresh.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateDeviceImage(shell);
+ // RawImage only allows us to rotate the image 90 degrees at the time,
+ // so to preserve the current rotation we must call getRotated()
+ // the same number of times the user has done it manually.
+ // TODO: improve the RawImage class.
+ for (int i=0; i < mRotateCount; i++) {
+ mRawImage = mRawImage.getRotated();
+ }
+ updateImageDisplay(shell);
+ }
+ });
+
+ // "rotate" button
+ Button rotate = new Button(shell, SWT.PUSH);
+ rotate.setText("Rotate");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ rotate.setLayoutData(data);
+ rotate.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mRawImage != null) {
+ mRotateCount = (mRotateCount + 1) % 4;
+ mRawImage = mRawImage.getRotated();
+ updateImageDisplay(shell);
+ }
+ }
+ });
+
+ // "save" button
+ mSave = new Button(shell, SWT.PUSH);
+ mSave.setText("Save");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ mSave.setLayoutData(data);
+ mSave.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ saveImage(shell);
+ }
+ });
+
+ Button copy = new Button(shell, SWT.PUSH);
+ copy.setText("Copy");
+ copy.setToolTipText("Copy the screenshot to the clipboard");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ copy.setLayoutData(data);
+ copy.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ copy();
+ }
+ });
+
+
+ // "done" button
+ Button done = new Button(shell, SWT.PUSH);
+ done.setText("Done");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ done.setLayoutData(data);
+ done.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ shell.close();
+ }
+ });
+
+ // title/"capturing" label
+ mBusyLabel = new Label(shell, SWT.NONE);
+ mBusyLabel.setText("Preparing...");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+ data.horizontalSpan = colCount;
+ mBusyLabel.setLayoutData(data);
+
+ // space for the image
+ mImageLabel = new Label(shell, SWT.BORDER);
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.horizontalSpan = colCount;
+ mImageLabel.setLayoutData(data);
+ Display display = shell.getDisplay();
+ mImageLabel.setImage(ImageLoader.createPlaceHolderArt(
+ display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)));
+
+
+ shell.setDefaultButton(done);
+ }
+
+ /**
+ * Copies the content of {@link #mImageLabel} to the clipboard.
+ */
+ private void copy() {
+ mClipboard.setContents(
+ new Object[] {
+ mImageLabel.getImage().getImageData()
+ }, new Transfer[] {
+ ImageTransfer.getInstance()
+ });
+ }
+
+ /**
+ * Captures a new image from the device, and display it.
+ */
+ private void updateDeviceImage(Shell shell) {
+ mBusyLabel.setText("Capturing..."); // no effect
+
+ shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
+
+ mRawImage = getDeviceImage();
+
+ updateImageDisplay(shell);
+ }
+
+ /**
+ * Updates the display with {@link #mRawImage}.
+ * @param shell
+ */
+ private void updateImageDisplay(Shell shell) {
+ Image image;
+ if (mRawImage == null) {
+ Display display = shell.getDisplay();
+ image = ImageLoader.createPlaceHolderArt(
+ display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE));
+
+ mSave.setEnabled(false);
+ mBusyLabel.setText("Screen not available");
+ } else {
+ // convert raw data to an Image.
+ PaletteData palette = new PaletteData(
+ mRawImage.getRedMask(),
+ mRawImage.getGreenMask(),
+ mRawImage.getBlueMask());
+
+ ImageData imageData = new ImageData(mRawImage.width, mRawImage.height,
+ mRawImage.bpp, palette, 1, mRawImage.data);
+ image = new Image(getParent().getDisplay(), imageData);
+
+ mSave.setEnabled(true);
+ mBusyLabel.setText("Captured image:");
+ }
+
+ mImageLabel.setImage(image);
+ mImageLabel.pack();
+ shell.pack();
+
+ // there's no way to restore old cursor; assume it's ARROW
+ shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
+ }
+
+ /**
+ * Grabs an image from an ADB-connected device and returns it as a {@link RawImage}.
+ */
+ private RawImage getDeviceImage() {
+ try {
+ return mDevice.getScreenshot();
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage());
+ return null;
+ } catch (TimeoutException e) {
+ Log.w("ddms", "Unable to get frame buffer: timeout ");
+ return null;
+ } catch (AdbCommandRejectedException e) {
+ Log.w("ddms", "Unable to get frame buffer: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /*
+ * Prompt the user to save the image to disk.
+ */
+ private void saveImage(Shell shell) {
+ FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+
+ Calendar now = Calendar.getInstance();
+ String fileName = String.format("device-%tF-%tH%tM%tS.png",
+ now, now, now, now);
+
+ dlg.setText("Save image...");
+ dlg.setFileName(fileName);
+
+ String lastDir = DdmUiPreferences.getStore().getString("lastImageSaveDir");
+ if (lastDir.length() == 0) {
+ lastDir = DdmUiPreferences.getStore().getString("imageSaveDir");
+ }
+ dlg.setFilterPath(lastDir);
+ dlg.setFilterNames(new String[] {
+ "PNG Files (*.png)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.png" //$NON-NLS-1$
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ // FileDialog.getFilterPath() does NOT always return the current
+ // directory of the FileDialog; on the Mac it sometimes just returns
+ // the value the dialog was initialized with. It does however return
+ // the full path as its return value, so just pick the path from
+ // there.
+ if (!fileName.endsWith(".png")) {
+ fileName = fileName + ".png";
+ }
+
+ String saveDir = new File(fileName).getParent();
+ if (saveDir != null) {
+ DdmUiPreferences.getStore().setValue("lastImageSaveDir", saveDir);
+ }
+
+ Log.d("ddms", "Saving image to " + fileName);
+ ImageData imageData = mImageLabel.getImage().getImageData();
+
+ try {
+ org.eclipse.swt.graphics.ImageLoader loader =
+ new org.eclipse.swt.graphics.ImageLoader();
+
+ loader.data = new ImageData[] { imageData };
+ loader.save(fileName, SWT.IMAGE_PNG);
+ }
+ catch (SWTException e) {
+ Log.w("ddms", "Unable to save " + fileName + ": " + e.getMessage());
+ }
+ }
+ }
+
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java
new file mode 100644
index 0000000..e6d2211
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+
+/**
+ * A Panel that requires {@link Device}/{@link Client} selection notifications.
+ */
+public abstract class SelectionDependentPanel extends Panel {
+ private IDevice mCurrentDevice = null;
+ private Client mCurrentClient = null;
+
+ /**
+ * Returns the current {@link Device}.
+ * @return the current device or null if none are selected.
+ */
+ protected final IDevice getCurrentDevice() {
+ return mCurrentDevice;
+ }
+
+ /**
+ * Returns the current {@link Client}.
+ * @return the current client or null if none are selected.
+ */
+ protected final Client getCurrentClient() {
+ return mCurrentClient;
+ }
+
+ /**
+ * Sent when a new device is selected.
+ * @param selectedDevice the selected device.
+ */
+ public final void deviceSelected(IDevice selectedDevice) {
+ if (selectedDevice != mCurrentDevice) {
+ mCurrentDevice = selectedDevice;
+ deviceSelected();
+ }
+ }
+
+ /**
+ * Sent when a new client is selected.
+ * @param selectedClient the selected client.
+ */
+ public final void clientSelected(Client selectedClient) {
+ if (selectedClient != mCurrentClient) {
+ mCurrentClient = selectedClient;
+ clientSelected();
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ public abstract void deviceSelected();
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ public abstract void clientSelected();
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
new file mode 100644
index 0000000..b00120b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IStackTraceInfo;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Stack Trace Panel.
+ * <p/>This is not a panel in the regular sense. Instead this is just an object around the creation
+ * and management of a Stack Trace display.
+ * <p/>UI creation is done through
+ * {@link #createPanel(Composite, String, IPreferenceStore)}.
+ *
+ */
+public final class StackTracePanel {
+
+ private static ISourceRevealer sSourceRevealer;
+
+ private Table mStackTraceTable;
+ private TableViewer mStackTraceViewer;
+
+ private Client mCurrentClient;
+
+
+ /**
+ * Content Provider to display the stack trace of a thread.
+ * Expected input is a {@link IStackTraceInfo} object.
+ */
+ private static class StackTraceContentProvider implements IStructuredContentProvider {
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof IStackTraceInfo) {
+ // getElement cannot return null, so we return an empty array
+ // if there's no stack trace
+ StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace();
+ if (trace != null) {
+ return trace;
+ }
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+
+ /**
+ * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be
+ * of type {@link StackTraceElement}.
+ */
+ private static class StackTraceLabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof StackTraceElement && columnIndex == 0) {
+ StackTraceElement traceElement = (StackTraceElement) element;
+ return " at " + traceElement.toString();
+ }
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide a method that is able to reveal a method
+ * in a source editor
+ */
+ public interface ISourceRevealer {
+ /**
+ * Sent to reveal a particular line in a source editor
+ * @param applicationName the name of the application running the source.
+ * @param className the fully qualified class name
+ * @param line the line to reveal
+ */
+ public void reveal(String applicationName, String className, int line);
+ }
+
+
+ /**
+ * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor.
+ * @param revealer
+ */
+ public static void setSourceRevealer(ISourceRevealer revealer) {
+ sSourceRevealer = revealer;
+ }
+
+ /**
+ * Creates the controls for the StrackTrace display.
+ * <p/>This method will set the parent {@link Composite} to use a {@link GridLayout} with
+ * 2 columns.
+ * @param parent the parent composite.
+ * @param prefs_stack_column
+ * @param store
+ */
+ public Table createPanel(Composite parent, String prefs_stack_column,
+ IPreferenceStore store) {
+
+ mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mStackTraceTable.setHeaderVisible(false);
+ mStackTraceTable.setLinesVisible(false);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Info",
+ SWT.LEFT,
+ "SomeLongClassName.method(android/somepackage/someotherpackage/somefile.java:99999)", //$NON-NLS-1$
+ prefs_stack_column, store);
+
+ mStackTraceViewer = new TableViewer(mStackTraceTable);
+ mStackTraceViewer.setContentProvider(new StackTraceContentProvider());
+ mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider());
+
+ mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ if (sSourceRevealer != null && mCurrentClient != null) {
+ // get the selected stack trace element
+ ISelection selection = mStackTraceViewer.getSelection();
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof StackTraceElement) {
+ StackTraceElement traceElement = (StackTraceElement)object;
+
+ if (traceElement.isNativeMethod() == false) {
+ sSourceRevealer.reveal(
+ mCurrentClient.getClientData().getClientDescription(),
+ traceElement.getClassName(),
+ traceElement.getLineNumber());
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return mStackTraceTable;
+ }
+
+ /**
+ * Sets the input for the {@link TableViewer}.
+ * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of
+ * {@link StackTraceElement}
+ */
+ public void setViewerInput(IStackTraceInfo input) {
+ mStackTraceViewer.setInput(input);
+ mStackTraceViewer.refresh();
+ }
+
+ /**
+ * Sets the current client running the stack trace.
+ * @param currentClient the {@link Client}.
+ */
+ public void setCurrentClient(Client currentClient) {
+ mCurrentClient = currentClient;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java
new file mode 100644
index 0000000..732de59
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Helper class to run a Sync in a {@link ProgressMonitorDialog}.
+ */
+public class SyncProgressHelper {
+
+ /**
+ * a runnable class run with an {@link ISyncProgressMonitor}.
+ */
+ public interface SyncRunnable {
+ /** Runs the sync action */
+ void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException;
+ /** close the {@link SyncService} */
+ void close();
+ }
+
+ /**
+ * Runs a {@link SyncRunnable} in a {@link ProgressMonitorDialog}.
+ * @param runnable The {@link SyncRunnable} to run.
+ * @param progressMessage the message to display in the progress dialog
+ * @param parentShell the parent shell for the progress dialog.
+ *
+ * @throws InvocationTargetException
+ * @throws InterruptedException
+ * @throws SyncException if an error happens during the push of the package on the device.
+ * @throws IOException
+ * @throws TimeoutException
+ */
+ public static void run(final SyncRunnable runnable, final String progressMessage,
+ final Shell parentShell)
+ throws InvocationTargetException, InterruptedException, SyncException, IOException,
+ TimeoutException {
+
+ final Exception[] result = new Exception[1];
+ new ProgressMonitorDialog(parentShell).run(true, true, new IRunnableWithProgress() {
+ @Override
+ public void run(IProgressMonitor monitor) {
+ try {
+ runnable.run(new SyncProgressMonitor(monitor, progressMessage));
+ } catch (Exception e) {
+ result[0] = e;
+ } finally {
+ runnable.close();
+ }
+ }
+ });
+
+ if (result[0] instanceof SyncException) {
+ SyncException se = (SyncException)result[0];
+ if (se.wasCanceled()) {
+ // no need to throw this
+ return;
+ }
+ throw se;
+ }
+
+ // just do some casting so that the method declaration matches what's thrown.
+ if (result[0] instanceof TimeoutException) {
+ throw (TimeoutException)result[0];
+ }
+
+ if (result[0] instanceof IOException) {
+ throw (IOException)result[0];
+ }
+
+ if (result[0] instanceof RuntimeException) {
+ throw (RuntimeException)result[0];
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java
new file mode 100644
index 0000000..4254f67
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Implementation of the {@link ISyncProgressMonitor} wrapping an Eclipse {@link IProgressMonitor}.
+ */
+public class SyncProgressMonitor implements ISyncProgressMonitor {
+
+ private IProgressMonitor mMonitor;
+ private String mName;
+
+ public SyncProgressMonitor(IProgressMonitor monitor, String name) {
+ mMonitor = monitor;
+ mName = name;
+ }
+
+ @Override
+ public void start(int totalWork) {
+ mMonitor.beginTask(mName, totalWork);
+ }
+
+ @Override
+ public void stop() {
+ mMonitor.done();
+ }
+
+ @Override
+ public void advance(int work) {
+ mMonitor.worked(work);
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return mMonitor.isCanceled();
+ }
+
+ @Override
+ public void startSubTask(String name) {
+ mMonitor.subTask(name);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
new file mode 100644
index 0000000..8ba2171
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NullOutputReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.general.DefaultPieDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Displays system information graphs obtained from a bugreport file or device.
+ */
+public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {
+
+ // UI components
+ private Label mLabel;
+ private Button mFetchButton;
+ private Combo mDisplayMode;
+
+ private DefaultPieDataset mDataset;
+ private DefaultCategoryDataset mBarDataSet;
+
+ private StackLayout mStackLayout;
+ private Composite mChartComposite;
+ private Composite mPieChartComposite;
+ private Composite mStackedBarComposite;
+
+ // The bugreport file to process
+ private File mDataFile;
+
+ // To get output from adb commands
+ private FileOutputStream mTempStream;
+
+ // Selects the current display: MODE_CPU, etc.
+ private int mMode = 0;
+ private String mGfxPackageName;
+
+ private static final int MODE_CPU = 0;
+ private static final int MODE_MEMINFO = 1;
+ private static final int MODE_GFXINFO = 2;
+
+ // argument to dumpsys; section in the bugreport holding the data
+ private static final String DUMP_COMMAND[] = {
+ "dumpsys cpuinfo",
+ "cat /proc/meminfo ; procrank",
+ "dumpsys gfxinfo",
+ };
+
+ private static final String CAPTIONS[] = {
+ "CPU load",
+ "Memory usage",
+ "Frame Render Time",
+ };
+
+ /** Shell property that controls whether graphics profiling is enabled or not. */
+ private static final String PROP_GFX_PROFILING = "debug.hwui.profile"; //$NON-NLS-1$
+
+ /**
+ * Generates the dataset to display.
+ *
+ * @param file The bugreport file to process.
+ */
+ private void generateDataset(File file) {
+ if (file == null) {
+ return;
+ }
+ try {
+ BufferedReader br = getBugreportReader(file);
+ if (mMode == MODE_CPU) {
+ readCpuDataset(br);
+ } else if (mMode == MODE_MEMINFO) {
+ readMeminfoDataset(br);
+ } else if (mMode == MODE_GFXINFO) {
+ readGfxInfoDataset(br);
+ }
+ br.close();
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed with
+ * {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ if (getCurrentDevice() != null) {
+ mFetchButton.setEnabled(true);
+ loadFromDevice();
+ } else {
+ mFetchButton.setEnabled(false);
+ }
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed with
+ * {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mDisplayMode.setFocus();
+ }
+
+ /**
+ * Fetches a new bugreport from the device and updates the display.
+ * Fetching is asynchronous. See also addOutput, flush, and isCancelled.
+ */
+ private void loadFromDevice() {
+ clearDataSet();
+
+ if (mMode == MODE_GFXINFO) {
+ boolean en = isGfxProfilingEnabled();
+ if (!en) {
+ if (enableGfxProfiling()) {
+ MessageDialog.openInformation(Display.getCurrent().getActiveShell(),
+ "DDMS",
+ "Graphics profiling was enabled on the device.\n" +
+ "It may be necessary to relaunch your application to see profile information.");
+ } else {
+ MessageDialog.openError(Display.getCurrent().getActiveShell(),
+ "DDMS",
+ "Unexpected error enabling graphics profiling on device.\n");
+ return;
+ }
+ }
+ }
+
+ final String command = getDumpsysCommand(mMode);
+ if (command == null) {
+ return;
+ }
+
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ initShellOutputBuffer();
+ if (mMode == MODE_MEMINFO) {
+ // Hack to add bugreport-style section header for meminfo
+ mTempStream.write("------ MEMORY INFO ------\n".getBytes());
+ }
+ getCurrentDevice().executeShellCommand(command, SysinfoPanel.this);
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ } catch (TimeoutException e) {
+ Log.e("DDMS", e);
+ } catch (AdbCommandRejectedException e) {
+ Log.e("DDMS", e);
+ } catch (ShellCommandUnresponsiveException e) {
+ Log.e("DDMS", e);
+ }
+ }
+ }, "Sysinfo Output Collector");
+ t.start();
+ }
+
+ private boolean isGfxProfilingEnabled() {
+ IDevice device = getCurrentDevice();
+ if (device == null) {
+ return false;
+ }
+
+ String prop;
+ try {
+ prop = device.getPropertySync(PROP_GFX_PROFILING);
+ return Boolean.valueOf(prop);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private boolean enableGfxProfiling() {
+ IDevice device = getCurrentDevice();
+ if (device == null) {
+ return false;
+ }
+
+ try {
+ device.executeShellCommand("setprop " + PROP_GFX_PROFILING + " true",
+ new NullOutputReceiver());
+ } catch (Exception e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private String getDumpsysCommand(int mode) {
+ if (mode == MODE_GFXINFO) {
+ Client c = getCurrentClient();
+ if (c == null) {
+ return null;
+ }
+
+ ClientData cd = c.getClientData();
+ if (cd == null) {
+ return null;
+ }
+
+ mGfxPackageName = cd.getClientDescription();
+ if (mGfxPackageName == null) {
+ return null;
+ }
+
+ return "dumpsys gfxinfo " + mGfxPackageName;
+ } else if (mode < DUMP_COMMAND.length) {
+ return DUMP_COMMAND[mode];
+ }
+
+ return null;
+ }
+
+ /**
+ * Initializes temporary output file for executeShellCommand().
+ *
+ * @throws IOException on file error
+ */
+ void initShellOutputBuffer() throws IOException {
+ mDataFile = File.createTempFile("ddmsfile", ".txt");
+ mDataFile.deleteOnExit();
+ mTempStream = new FileOutputStream(mDataFile);
+ }
+
+ /**
+ * Adds output to the temp file. IShellOutputReceiver method. Called by
+ * executeShellCommand().
+ */
+ @Override
+ public void addOutput(byte[] data, int offset, int length) {
+ try {
+ mTempStream.write(data, offset, length);
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Processes output from shell command. IShellOutputReceiver method. The
+ * output is passed to generateDataset(). Called by executeShellCommand() on
+ * completion.
+ */
+ @Override
+ public void flush() {
+ if (mTempStream != null) {
+ try {
+ mTempStream.close();
+ generateDataset(mDataFile);
+ mTempStream = null;
+ mDataFile = null;
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+ }
+
+ /**
+ * IShellOutputReceiver method.
+ *
+ * @return false - don't cancel
+ */
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ /**
+ * Create our controls for the UI panel.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, false));
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Composite buttons = new Composite(top, SWT.NONE);
+ buttons.setLayout(new RowLayout());
+
+ mDisplayMode = new Combo(buttons, SWT.PUSH);
+ for (String mode : CAPTIONS) {
+ mDisplayMode.add(mode);
+ }
+ mDisplayMode.select(mMode);
+ mDisplayMode.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mMode = mDisplayMode.getSelectionIndex();
+ if (mDataFile != null) {
+ generateDataset(mDataFile);
+ } else if (getCurrentDevice() != null) {
+ loadFromDevice();
+ }
+ }
+ });
+
+ mFetchButton = new Button(buttons, SWT.PUSH);
+ mFetchButton.setText("Update from Device");
+ mFetchButton.setEnabled(false);
+ mFetchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ loadFromDevice();
+ }
+ });
+
+ mLabel = new Label(top, SWT.NONE);
+ mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mChartComposite = new Composite(top, SWT.NONE);
+ mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mStackLayout = new StackLayout();
+ mChartComposite.setLayout(mStackLayout);
+
+ mPieChartComposite = createPieChartComposite(mChartComposite);
+ mStackedBarComposite = createStackedBarComposite(mChartComposite);
+
+ mStackLayout.topControl = mPieChartComposite;
+
+ return top;
+ }
+
+ private Composite createStackedBarComposite(Composite chartComposite) {
+ mBarDataSet = new DefaultCategoryDataset();
+ JFreeChart chart = ChartFactory.createStackedBarChart("Per Frame Rendering Time",
+ "Frame #", "Time (ms)", mBarDataSet, PlotOrientation.VERTICAL,
+ true /* legend */, true /* tooltips */, false /* urls */);
+
+ ChartComposite c = newChartComposite(chart, chartComposite);
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return c;
+ }
+
+ private Composite createPieChartComposite(Composite chartComposite) {
+ mDataset = new DefaultPieDataset();
+ JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
+ /* legend */, true/* tooltips */, false /* urls */);
+
+ ChartComposite c = newChartComposite(chart, chartComposite);
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return c;
+ }
+
+ private ChartComposite newChartComposite(JFreeChart chart, Composite parent) {
+ return new ChartComposite(parent,
+ SWT.BORDER, chart,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000,
+ // max draw width. We don't want it to zoom, so we put a big number
+ 3000,
+ // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ false, // zoom
+ true);
+ }
+
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ // Don't care
+ }
+
+ /**
+ * Helper to open a bugreport and skip to the specified section.
+ *
+ * @param file File to open
+ * @return Reader to bugreport file
+ * @throws java.io.IOException on file error
+ */
+ private BufferedReader getBugreportReader(File file) throws
+ IOException {
+ return new BufferedReader(new FileReader(file));
+ }
+
+ /**
+ * Parse the time string generated by BatteryStats.
+ * A typical new-format string is "11d 13h 45m 39s 999ms".
+ * A typical old-format string is "12.3 sec".
+ * @return time in ms
+ */
+ private static long parseTimeMs(String s) {
+ long total = 0;
+ // Matches a single component e.g. "12.3 sec" or "45ms"
+ Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
+ Matcher m = p.matcher(s);
+ while (m.find()) {
+ String label = m.group(2);
+ if ("sec".equals(label)) {
+ // Backwards compatibility with old time format
+ total += (long) (Double.parseDouble(m.group(1)) * 1000);
+ continue;
+ }
+ long value = Integer.parseInt(m.group(1));
+ if ("d".equals(label)) {
+ total += value * 24 * 60 * 60 * 1000;
+ } else if ("h".equals(label)) {
+ total += value * 60 * 60 * 1000;
+ } else if ("m".equals(label)) {
+ total += value * 60 * 1000;
+ } else if ("s".equals(label)) {
+ total += value * 1000;
+ } else if ("ms".equals(label)) {
+ total += value;
+ }
+ }
+ return total;
+ }
+
+ public static final class BugReportParser {
+ public static final class DataValue {
+ final String name;
+ final double value;
+
+ public DataValue(String n, double v) {
+ name = n;
+ value = v;
+ }
+ };
+
+ /** Components of the time it takes to draw a single frame. */
+ public static final class GfxProfileData {
+ /** draw time (time spent building display lists) in ms */
+ final double draw;
+
+ /** process time (time spent by Android's 2D renderer to execute display lists) (ms) */
+ final double process;
+
+ /** execute time (time spent to send frame to the compositor) in ms */
+ final double execute;
+
+ public GfxProfileData(double draw, double process, double execute) {
+ this.draw = draw;
+ this.process = process;
+ this.execute = execute;
+ }
+ }
+
+ public static List<GfxProfileData> parseGfxInfo(BufferedReader br) throws IOException {
+ Pattern headerPattern = Pattern.compile("\\s+Draw\\s+Process\\s+Execute");
+
+ String line = null;
+ while ((line = br.readLine()) != null) {
+ Matcher m = headerPattern.matcher(line);
+ if (m.find()) {
+ break;
+ }
+ }
+
+ if (line == null) {
+ return Collections.emptyList();
+ }
+
+ // parse something like: " 0.85 1.10 0.61\n", 3 doubles basically
+ Pattern dataPattern =
+ Pattern.compile("(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)");
+
+ List<GfxProfileData> data = new ArrayList<BugReportParser.GfxProfileData>(128);
+ while ((line = br.readLine()) != null) {
+ Matcher m = dataPattern.matcher(line);
+ if (!m.find()) {
+ break;
+ }
+
+ double draw = safeParseDouble(m.group(1));
+ double process = safeParseDouble(m.group(2));
+ double execute = safeParseDouble(m.group(3));
+
+ data.add(new GfxProfileData(draw, process, execute));
+ }
+
+ return data;
+ }
+
+ /**
+ * Processes wakelock information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ public static List<DataValue> readWakelockDataset(BufferedReader br) throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+
+ Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
+ Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
+ double total = 0;
+ boolean inCurrent = false;
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith("Current Battery Usage Statistics")) {
+ inCurrent = true;
+ } else if (inCurrent) {
+ Matcher m = lockPattern.matcher(line);
+ if (m.find()) {
+ double value = parseTimeMs(m.group(2)) / 1000.;
+ results.add(new DataValue(m.group(1), value));
+ total -= value;
+ } else {
+ m = totalPattern.matcher(line);
+ if (m.find()) {
+ total += parseTimeMs(m.group(1)) / 1000.;
+ }
+ }
+ }
+ }
+ if (total > 0) {
+ results.add(new DataValue("Unlocked", total));
+ }
+
+ return results;
+ }
+
+ /**
+ * Processes alarm information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ public static List<DataValue> readAlarmDataset(BufferedReader br) throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+ Pattern pattern = Pattern.compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ long count = Long.parseLong(m.group(1));
+ String name = m.group(2);
+ results.add(new DataValue(name, count));
+ }
+ }
+
+ return results;
+ }
+
+ /**
+ * Processes cpu load information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ public static List<DataValue> readCpuDataset(BufferedReader br) throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+ Pattern pattern1 = Pattern.compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
+ Pattern pattern2 = Pattern.compile("(\\S+)% (\\S+): (.+)% user . (.+)% kernel");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ break;
+ }
+ line = line.trim();
+
+ if (line.startsWith("Load:")) {
+ continue;
+ }
+
+ String name = "";
+ double user = 0, kernel = 0, both = 0;
+ boolean found = false;
+
+ // try pattern1
+ Matcher m = pattern1.matcher(line);
+ if (m.find()) {
+ found = true;
+ name = m.group(1);
+ both = safeParseLong(m.group(2));
+ user = safeParseLong(m.group(3));
+ kernel = safeParseLong(m.group(4));
+ }
+
+ // try pattern2
+ m = pattern2.matcher(line);
+ if (m.find()) {
+ found = true;
+ name = m.group(2);
+ both = safeParseDouble(m.group(1));
+ user = safeParseDouble(m.group(3));
+ kernel = safeParseDouble(m.group(4));
+ }
+
+ if (!found) {
+ continue;
+ }
+
+ if ("TOTAL".equals(name)) {
+ if (both < 100) {
+ results.add(new DataValue("Idle", (100 - both)));
+ }
+ } else {
+ // Try to make graphs more useful even with rounding;
+ // log often has 0% user + 0% kernel = 1% total
+ // We arbitrarily give extra to kernel
+ if (user > 0) {
+ results.add(new DataValue(name + " (user)", user));
+ }
+ if (kernel > 0) {
+ results.add(new DataValue(name + " (kernel)" , both - user));
+ }
+ if (user == 0 && kernel == 0 && both > 0) {
+ results.add(new DataValue(name, both));
+ }
+ }
+
+ }
+
+ return results;
+ }
+
+ private static long safeParseLong(String s) {
+ try {
+ return Long.parseLong(s);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static double safeParseDouble(String s) {
+ try {
+ return Double.parseDouble(s);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Processes meminfo information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ public static List<DataValue> readMeminfoDataset(BufferedReader br) throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+ Pattern valuePattern = Pattern.compile("(\\d+) kB");
+ long total = 0;
+ long other = 0;
+
+ // Scan meminfo
+ String line = null;
+ while ((line = br.readLine()) != null) {
+ if (line.contains("----")) {
+ continue;
+ }
+
+ Matcher m = valuePattern.matcher(line);
+ if (m.find()) {
+ long kb = Long.parseLong(m.group(1));
+ if (line.startsWith("MemTotal")) {
+ total = kb;
+ } else if (line.startsWith("MemFree")) {
+ results.add(new DataValue("Free", kb));
+ total -= kb;
+ } else if (line.startsWith("Slab")) {
+ results.add(new DataValue("Slab", kb));
+ total -= kb;
+ } else if (line.startsWith("PageTables")) {
+ results.add(new DataValue("PageTables", kb));
+ total -= kb;
+ } else if (line.startsWith("Buffers") && kb > 0) {
+ results.add(new DataValue("Buffers", kb));
+ total -= kb;
+ } else if (line.startsWith("Inactive")) {
+ results.add(new DataValue("Inactive", kb));
+ total -= kb;
+ } else if (line.startsWith("MemFree")) {
+ results.add(new DataValue("Free", kb));
+ total -= kb;
+ }
+ } else {
+ break;
+ }
+ }
+
+ List<DataValue> procRankResults = readProcRankDataset(br, line);
+ for (DataValue procRank : procRankResults) {
+ if (procRank.value > 2000) { // only show processes using > 2000K in memory
+ results.add(procRank);
+ } else {
+ other += procRank.value;
+ }
+
+ total -= procRank.value;
+ }
+
+ if (other > 0) {
+ results.add(new DataValue("Other", other));
+ }
+
+ // The Pss calculation is not necessarily accurate as accounting memory to
+ // a process is not accurate. So only if there really is unaccounted for memory do we
+ // add it to the pie.
+ if (total > 0) {
+ results.add(new DataValue("Unknown", total));
+ }
+
+ return results;
+ }
+
+ static List<DataValue> readProcRankDataset(BufferedReader br, String header)
+ throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+
+ if (header == null || !header.contains("PID")) {
+ return results;
+ }
+
+ Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
+ List<String> fields = Lists.newArrayList(PROCRANK_SPLITTER.split(header));
+ int pssIndex = fields.indexOf("Pss");
+ int cmdIndex = fields.indexOf("cmdline");
+
+ if (pssIndex == -1 || cmdIndex == -1) {
+ return results;
+ }
+
+ String line;
+ while ((line = br.readLine()) != null) {
+ // Extract pss field from procrank output
+ fields = Lists.newArrayList(PROCRANK_SPLITTER.split(line));
+
+ if (fields.size() < cmdIndex) {
+ break;
+ }
+
+ String cmdline = fields.get(cmdIndex).replace("/system/bin/", "");
+ String pssInK = fields.get(pssIndex);
+ if (pssInK.endsWith("K")) {
+ pssInK = pssInK.substring(0, pssInK.length() - 1);
+ }
+ long pss = safeParseLong(pssInK);
+ results.add(new DataValue(cmdline, pss));
+ }
+
+ return results;
+ }
+
+ /**
+ * Processes sync information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ public static List<DataValue> readSyncDataset(BufferedReader br) throws IOException {
+ List<DataValue> results = new ArrayList<DataValue>();
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith(" |") && line.length() > 70) {
+ String authority = line.substring(3, 18).trim();
+ String duration = line.substring(61, 70).trim();
+ // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
+ String durParts[] = duration.split(":");
+ if (durParts.length == 2) {
+ long dur = Long.parseLong(durParts[0]) * 60 + Long
+ .parseLong(durParts[1]);
+ results.add(new DataValue(authority, dur));
+ } else if (duration.length() == 3) {
+ long dur = Long.parseLong(durParts[0]) * 3600
+ + Long.parseLong(durParts[1]) * 60 + Long
+ .parseLong(durParts[2]);
+ results.add(new DataValue(authority, dur));
+ }
+ }
+ }
+
+ return results;
+ }
+ }
+
+ private void readCpuDataset(BufferedReader br) throws IOException {
+ updatePieDataSet(BugReportParser.readCpuDataset(br), "");
+ }
+
+ private void readMeminfoDataset(BufferedReader br) throws IOException {
+ updatePieDataSet(BugReportParser.readMeminfoDataset(br), "PSS in kB");
+ }
+
+ private void readGfxInfoDataset(BufferedReader br) throws IOException {
+ updateBarChartDataSet(BugReportParser.parseGfxInfo(br),
+ mGfxPackageName == null ? "" : mGfxPackageName);
+ }
+
+ private void clearDataSet() {
+ mLabel.setText("");
+ mDataset.clear();
+ mBarDataSet.clear();
+ }
+
+ private void updatePieDataSet(final List<BugReportParser.DataValue> data, final String label) {
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ mLabel.setText(label);
+ mStackLayout.topControl = mPieChartComposite;
+ mChartComposite.layout();
+
+ for (BugReportParser.DataValue d : data) {
+ mDataset.setValue(d.name, d.value);
+ }
+ }
+ });
+ }
+
+ private void updateBarChartDataSet(final List<GfxProfileData> gfxProfileData,
+ final String label) {
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ mLabel.setText(label);
+ mStackLayout.topControl = mStackedBarComposite;
+ mChartComposite.layout();
+
+ for (int i = 0; i < gfxProfileData.size(); i++) {
+ GfxProfileData d = gfxProfileData.get(i);
+ String frameNumber = Integer.toString(i);
+
+ mBarDataSet.addValue(d.draw, "Draw", frameNumber);
+ mBarDataSet.addValue(d.process, "Process", frameNumber);
+ mBarDataSet.addValue(d.execute, "Execute", frameNumber);
+ }
+ }
+ });
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java
new file mode 100644
index 0000000..66dcc0a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Utility class to help using Table objects.
+ *
+ */
+public final class TableHelper {
+ /**
+ * Create a TableColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> object and a preference entry name String
+ * object are provided then the column will listen to change in its width
+ * and update the preference store accordingly.
+ *
+ * @param parent The Table parent object
+ * @param header The header string
+ * @param style The column style
+ * @param sample_text A sample text to figure out column width if preference
+ * value is missing
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ * @return The TableColumn object that was created
+ */
+ public static TableColumn createTableColumn(Table parent, String header,
+ int style, String sample_text, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TableColumn col = new TableColumn(parent, style);
+
+ // if there is no pref store or the entry is missing, we use the sample
+ // text and pack the column.
+ // Otherwise we just read the width from the prefs and apply it.
+ if (prefs == null || prefs.contains(pref_name) == false) {
+ col.setText(sample_text);
+ col.pack();
+
+ // init the prefs store with the current value
+ if (prefs != null) {
+ prefs.setValue(pref_name, col.getWidth());
+ }
+ } else {
+ col.setWidth(prefs.getInt(pref_name));
+ }
+
+ // set the header
+ col.setText(header);
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put store the new width value.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TableColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+
+ return col;
+ }
+
+ /**
+ * Create a TreeColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> object and a preference entry name String
+ * object are provided then the column will listen to change in its width
+ * and update the preference store accordingly.
+ *
+ * @param parent The Table parent object
+ * @param header The header string
+ * @param style The column style
+ * @param sample_text A sample text to figure out column width if preference
+ * value is missing
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ */
+ public static void createTreeColumn(Tree parent, String header, int style,
+ String sample_text, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TreeColumn col = new TreeColumn(parent, style);
+
+ // if there is no pref store or the entry is missing, we use the sample
+ // text and pack the column.
+ // Otherwise we just read the width from the prefs and apply it.
+ if (prefs == null || prefs.contains(pref_name) == false) {
+ col.setText(sample_text);
+ col.pack();
+
+ // init the prefs store with the current value
+ if (prefs != null) {
+ prefs.setValue(pref_name, col.getWidth());
+ }
+ } else {
+ col.setWidth(prefs.getInt(pref_name));
+ }
+
+ // set the header
+ col.setText(header);
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put store the new width value.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TreeColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+ }
+
+ /**
+ * Create a TreeColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> object and a preference entry name String
+ * object are provided then the column will listen to change in its width
+ * and update the preference store accordingly.
+ *
+ * @param parent The Table parent object
+ * @param header The header string
+ * @param style The column style
+ * @param width the width of the column if the preference value is missing
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ */
+ public static void createTreeColumn(Tree parent, String header, int style,
+ int width, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TreeColumn col = new TreeColumn(parent, style);
+
+ // if there is no pref store or the entry is missing, we use the sample
+ // text and pack the column.
+ // Otherwise we just read the width from the prefs and apply it.
+ if (prefs == null || prefs.contains(pref_name) == false) {
+ col.setWidth(width);
+
+ // init the prefs store with the current value
+ if (prefs != null) {
+ prefs.setValue(pref_name, width);
+ }
+ } else {
+ col.setWidth(prefs.getInt(pref_name));
+ }
+
+ // set the header
+ col.setText(header);
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put store the new width value.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TreeColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java
new file mode 100644
index 0000000..c1eb7f6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.Arrays;
+
+/**
+ * Base class for panel containing Table that need to support copy-paste-selectAll
+ */
+public abstract class TablePanel extends ClientDisplayPanel {
+ private ITableFocusListener mGlobalListener;
+
+ /**
+ * Sets a TableFocusListener which will be notified when one of the tables
+ * gets or loses focus.
+ *
+ * @param listener
+ */
+ public void setTableFocusListener(ITableFocusListener listener) {
+ // record the global listener, to make sure table created after
+ // this call will still be setup.
+ mGlobalListener = listener;
+
+ setTableFocusListener();
+ }
+
+ /**
+ * Sets up the Table of object of the panel to work with the global listener.<br>
+ * Default implementation does nothing.
+ */
+ protected void setTableFocusListener() {
+
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.
+ *
+ * @param table the Table object.
+ * @param colStart
+ * @param colEnd
+ */
+ protected final void addTableToFocusListener(final Table table,
+ final int colStart, final int colEnd) {
+ // create the activator for this table
+ final IFocusedTableActivator activator = new IFocusedTableActivator() {
+ @Override
+ public void copy(Clipboard clipboard) {
+ int[] selection = table.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // all lines must be concatenated.
+ StringBuilder sb = new StringBuilder();
+
+ // loop on the selection and output the file.
+ for (int i : selection) {
+ TableItem item = table.getItem(i);
+ for (int c = colStart ; c <= colEnd ; c++) {
+ sb.append(item.getText(c));
+ sb.append('\t');
+ }
+ sb.append('\n');
+ }
+
+ // now add that to the clipboard if the string has content
+ String data = sb.toString();
+ if (data != null && data.length() > 0) {
+ clipboard.setContents(
+ new Object[] { data },
+ new Transfer[] { TextTransfer.getInstance() });
+ }
+ }
+
+ @Override
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mGlobalListener.focusLost(activator);
+ }
+ });
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.<br>
+ * When the copy method is invoked, all columns are put in the clipboard, separated
+ * by tabs
+ *
+ * @param table the Table object.
+ */
+ protected final void addTableToFocusListener(final Table table) {
+ addTableToFocusListener(table, 0, table.getColumnCount()-1);
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
new file mode 100644
index 0000000..81e245d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ThreadInfo;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Base class for our information panels.
+ */
+public class ThreadPanel extends TablePanel {
+
+ private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$
+
+ private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$
+
+ private static final String PREFS_STACK_COLUMN = "threadPanel.stack.col0"; //$NON-NLS-1$
+
+ private Display mDisplay;
+ private Composite mBase;
+ private Label mNotEnabled;
+ private Label mNotSelected;
+
+ private Composite mThreadBase;
+ private Table mThreadTable;
+ private TableViewer mThreadViewer;
+
+ private Composite mStackTraceBase;
+ private Button mRefreshStackTraceButton;
+ private Label mStackTraceTimeLabel;
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+
+ /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */
+ private boolean mMustStopRecurringThreadUpdate = false;
+ /** Flag to tell the recurring thread update to stop running */
+ private boolean mRecurringThreadUpdateRunning = false;
+
+ private Object mLock = new Object();
+
+ private static final String[] THREAD_STATUS = {
+ "Zombie", "Runnable", "TimedWait", "Monitor",
+ "Wait", "Initializing", "Starting", "Native", "VmWait",
+ "Suspended"
+ };
+
+ /**
+ * Content Provider to display the threads of a client.
+ * Expected input is a {@link Client} object.
+ */
+ private static class ThreadContentProvider implements IStructuredContentProvider {
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ return ((Client)inputElement).getClientData().getThreads();
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+
+ /**
+ * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be
+ * of type {@link ThreadInfo}.
+ */
+ private static class ThreadLabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof ThreadInfo) {
+ ThreadInfo thread = (ThreadInfo)element;
+ switch (columnIndex) {
+ case 0:
+ return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$
+ String.valueOf(thread.getThreadId());
+ case 1:
+ return String.valueOf(thread.getTid());
+ case 2:
+ if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length)
+ return THREAD_STATUS[thread.getStatus()];
+ return "unknown";
+ case 3:
+ return String.valueOf(thread.getUtime());
+ case 4:
+ return String.valueOf(thread.getStime());
+ case 5:
+ return thread.getThreadName();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ mBase = new Composite(parent, SWT.NONE);
+ mBase.setLayout(new StackLayout());
+
+ // UI for thread not enabled
+ mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP);
+ mNotEnabled.setText("Thread updates not enabled for selected client\n"
+ + "(use toolbar button to enable)");
+
+ // UI for not client selected
+ mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP);
+ mNotSelected.setText("no client is selected");
+
+ // base composite for selected client with enabled thread update.
+ mThreadBase = new Composite(mBase, SWT.NONE);
+ mThreadBase.setLayout(new FormLayout());
+
+ // table above the sash
+ mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION);
+ mThreadTable.setHeaderVisible(true);
+ mThreadTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "ID",
+ SWT.RIGHT,
+ "888", //$NON-NLS-1$
+ PREFS_THREAD_COL_ID, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Tid",
+ SWT.RIGHT,
+ "88888", //$NON-NLS-1$
+ PREFS_THREAD_COL_TID, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Status",
+ SWT.LEFT,
+ "timed-wait", //$NON-NLS-1$
+ PREFS_THREAD_COL_STATUS, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "utime",
+ SWT.RIGHT,
+ "utime", //$NON-NLS-1$
+ PREFS_THREAD_COL_UTIME, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "stime",
+ SWT.RIGHT,
+ "utime", //$NON-NLS-1$
+ PREFS_THREAD_COL_STIME, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Name",
+ SWT.LEFT,
+ "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$
+ PREFS_THREAD_COL_NAME, store);
+
+ mThreadViewer = new TableViewer(mThreadTable);
+ mThreadViewer.setContentProvider(new ThreadContentProvider());
+ mThreadViewer.setLabelProvider(new ThreadLabelProvider());
+
+ mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ requestThreadStackTrace(getThreadSelection(event.getSelection()));
+ }
+ });
+ mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ requestThreadStackTrace(getThreadSelection(event.getSelection()));
+ }
+ });
+
+ // the separating sash
+ final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL);
+ Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+ sash.setBackground(darkGray);
+
+ // the UI below the sash
+ mStackTraceBase = new Composite(mThreadBase, SWT.NONE);
+ mStackTraceBase.setLayout(new GridLayout(2, false));
+
+ mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH);
+ mRefreshStackTraceButton.setText("Refresh");
+ mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ requestThreadStackTrace(getThreadSelection(null));
+ }
+ });
+
+ mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
+ mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, PREFS_STACK_COLUMN, store);
+
+ GridData gd;
+ mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 2;
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mThreadTable.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_THREAD_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceBase.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = mThreadBase.getClientArea();
+ int bottom = panelRect.height - sashRect.height - 100;
+ e.y = Math.max(Math.min(e.y, bottom), 100);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ store.setValue(PREFS_THREAD_SASH, e.y);
+ mThreadBase.layout();
+ }
+ }
+ });
+
+ ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+
+ return mBase;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mThreadTable.setFocus();
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 ||
+ (changeMask & Client.CHANGE_THREAD_DATA) != 0) {
+ try {
+ mThreadTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ clientSelected();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) {
+ try {
+ mThreadTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateThreadStackCall();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mThreadTable.isDisposed()) {
+ return;
+ }
+
+ Client client = getCurrentClient();
+
+ mStackTracePanel.setCurrentClient(client);
+
+ if (client != null) {
+ if (!client.isThreadUpdateEnabled()) {
+ ((StackLayout)mBase.getLayout()).topControl = mNotEnabled;
+ mThreadViewer.setInput(null);
+
+ // if we are currently updating the thread, stop doing it.
+ mMustStopRecurringThreadUpdate = true;
+ } else {
+ ((StackLayout)mBase.getLayout()).topControl = mThreadBase;
+ mThreadViewer.setInput(client);
+
+ synchronized (mLock) {
+ // if we're not updating we start the process
+ if (mRecurringThreadUpdateRunning == false) {
+ startRecurringThreadUpdate();
+ } else if (mMustStopRecurringThreadUpdate) {
+ // else if there's a runnable that's still going to get called, lets
+ // simply cancel the stop, and keep going
+ mMustStopRecurringThreadUpdate = false;
+ }
+ }
+ }
+ } else {
+ ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+ mThreadViewer.setInput(null);
+ }
+
+ mBase.layout();
+ }
+
+ private void requestThreadStackTrace(ThreadInfo selectedThread) {
+ if (selectedThread != null) {
+ Client client = (Client) mThreadViewer.getInput();
+ if (client != null) {
+ client.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+
+ /**
+ * Updates the stack call of the currently selected thread.
+ * <p/>
+ * This <b>must</b> be called from the UI thread.
+ */
+ private void updateThreadStackCall() {
+ Client client = getCurrentClient();
+ if (client != null) {
+ // get the current selection in the ThreadTable
+ ThreadInfo selectedThread = getThreadSelection(null);
+
+ if (selectedThread != null) {
+ updateThreadStackTrace(selectedThread);
+ } else {
+ updateThreadStackTrace(null);
+ }
+ }
+ }
+
+ /**
+ * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied
+ * of current data.
+ * @param thread
+ */
+ private void updateThreadStackTrace(ThreadInfo thread) {
+ mStackTracePanel.setViewerInput(thread);
+
+ if (thread != null) {
+ mRefreshStackTraceButton.setEnabled(true);
+ long stackcallTime = thread.getStackCallTime();
+ if (stackcallTime != 0) {
+ String label = new Date(stackcallTime).toString();
+ mStackTraceTimeLabel.setText(label);
+ } else {
+ mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ mRefreshStackTraceButton.setEnabled(true);
+ mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mThreadTable);
+ addTableToFocusListener(mStackTraceTable);
+ }
+
+ /**
+ * Initiate recurring events. We use a shorter "initialWait" so we do the
+ * first execution sooner. We don't do it immediately because we want to
+ * give the clients a chance to get set up.
+ */
+ private void startRecurringThreadUpdate() {
+ mRecurringThreadUpdateRunning = true;
+ int initialWait = 1000;
+
+ mDisplay.timerExec(initialWait, new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ // lets check we still want updates.
+ if (mMustStopRecurringThreadUpdate == false) {
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestThreadUpdate();
+
+ mDisplay.timerExec(
+ DdmUiPreferences.getThreadRefreshInterval() * 1000, this);
+ } else {
+ // we don't have a Client, which means the runnable is not
+ // going to be called through the timer. We reset the running flag.
+ mRecurringThreadUpdateRunning = false;
+ }
+ } else {
+ // else actually stops (don't call the timerExec) and reset the flags.
+ mRecurringThreadUpdateRunning = false;
+ mMustStopRecurringThreadUpdate = false;
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the current thread selection or <code>null</code> if none is found.
+ * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection
+ * is returned, otherwise, the <code>ISelection</code> returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or <code>null</code>
+ */
+ private ThreadInfo getThreadSelection(ISelection selection) {
+ if (selection == null) {
+ selection = mThreadViewer.getSelection();
+ }
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof ThreadInfo) {
+ return (ThreadInfo)object;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java
new file mode 100644
index 0000000..856b874
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.actions;
+
+/**
+ * Common interface for basic action handling. This allows the common ui
+ * components to access ToolItem or Action the same way.
+ */
+public interface ICommonAction {
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ */
+ public void setEnabled(boolean enabled);
+
+ /**
+ * Sets the checked status of this action.
+ * @param checked the new checked status
+ */
+ public void setChecked(boolean checked);
+
+ /**
+ * Sets the {@link Runnable} that will be executed when the action is triggered.
+ */
+ public void setRunnable(Runnable runnable);
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java
new file mode 100644
index 0000000..c7fef32
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.actions;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+/**
+ * Wrapper around {@link ToolItem} to implement {@link ICommonAction}
+ */
+public class ToolItemAction implements ICommonAction {
+ public ToolItem item;
+
+ public ToolItemAction(ToolBar parent, int style) {
+ item = new ToolItem(parent, style);
+ }
+
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ * @see ICommonAction#setChecked(boolean)
+ */
+ @Override
+ public void setChecked(boolean checked) {
+ item.setSelection(checked);
+ }
+
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ * @see ICommonAction#setEnabled(boolean)
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ item.setEnabled(enabled);
+ }
+
+ /**
+ * Sets the {@link Runnable} that will be executed when the action is triggered (through
+ * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}).
+ * @see ICommonAction#setRunnable(Runnable)
+ */
+ @Override
+ public void setRunnable(final Runnable runnable) {
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ runnable.run();
+ }
+ });
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java
new file mode 100644
index 0000000..8e9e11b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface UiThread {
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java
new file mode 100644
index 0000000..e767eda
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are not executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface WorkerThread {
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java
new file mode 100644
index 0000000..4df4376
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.console;
+
+
+/**
+ * Static Console used to ouput messages. By default outputs the message to System.out and
+ * System.err, but can receive a IDdmConsole object which will actually do something.
+ */
+public class DdmConsole {
+
+ private static IDdmConsole mConsole;
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printErrorToConsole(String message) {
+ if (mConsole != null) {
+ mConsole.printErrorToConsole(message);
+ } else {
+ System.err.println(message);
+ }
+ }
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printErrorToConsole(String[] messages) {
+ if (mConsole != null) {
+ mConsole.printErrorToConsole(messages);
+ } else {
+ for (String message : messages) {
+ System.err.println(message);
+ }
+ }
+ }
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printToConsole(String message) {
+ if (mConsole != null) {
+ mConsole.printToConsole(message);
+ } else {
+ System.out.println(message);
+ }
+ }
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printToConsole(String[] messages) {
+ if (mConsole != null) {
+ mConsole.printToConsole(messages);
+ } else {
+ for (String message : messages) {
+ System.out.println(message);
+ }
+ }
+ }
+
+ /**
+ * Sets a IDdmConsole to override the default behavior of the console
+ * @param console The new IDdmConsole
+ * **/
+ public static void setConsole(IDdmConsole console) {
+ mConsole = console;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java
new file mode 100644
index 0000000..3679d41
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.console;
+
+
+/**
+ * DDMS console interface.
+ */
+public interface IDdmConsole {
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ */
+ public void printErrorToConsole(String message);
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ */
+ public void printErrorToConsole(String[] messages);
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ */
+ public void printToConsole(String message);
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ */
+ public void printToConsole(String[] messages);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java
new file mode 100644
index 0000000..062d4f0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.FileListingService.IListingReceiver;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+
+/**
+ * Content provider class for device Explorer.
+ */
+class DeviceContentProvider implements ITreeContentProvider {
+
+ private TreeViewer mViewer;
+ private FileListingService mFileListingService;
+ private FileEntry mRootEntry;
+
+ private IListingReceiver sListingReceiver = new IListingReceiver() {
+ @Override
+ public void setChildren(final FileEntry entry, FileEntry[] children) {
+ final Tree t = mViewer.getTree();
+ if (t != null && t.isDisposed() == false) {
+ Display display = t.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (t.isDisposed() == false) {
+ // refresh the entry.
+ mViewer.refresh(entry);
+
+ // force it open, since on linux and windows
+ // when getChildren() returns null, the node is
+ // not considered expanded.
+ mViewer.setExpandedState(entry, true);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void refreshEntry(final FileEntry entry) {
+ final Tree t = mViewer.getTree();
+ if (t != null && t.isDisposed() == false) {
+ Display display = t.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (t.isDisposed() == false) {
+ // refresh the entry.
+ mViewer.refresh(entry);
+ }
+ }
+ });
+ }
+ }
+ }
+ };
+
+ /**
+ *
+ */
+ public DeviceContentProvider() {
+ }
+
+ public void setListingService(FileListingService fls) {
+ mFileListingService = fls;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
+ */
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof FileEntry) {
+ FileEntry parentEntry = (FileEntry)parentElement;
+
+ Object[] oldEntries = parentEntry.getCachedChildren();
+ Object[] newEntries = mFileListingService.getChildren(parentEntry,
+ true, sListingReceiver);
+
+ if (newEntries != null) {
+ return newEntries;
+ } else {
+ // if null was returned, this means the cache was not valid,
+ // and a thread was launched for ls. sListingReceiver will be
+ // notified with the new entries.
+ return oldEntries;
+ }
+ }
+ return new Object[0];
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
+ */
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ return entry.getParent();
+ }
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
+ */
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ return entry.getType() == FileListingService.TYPE_DIRECTORY;
+ }
+ return false;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
+ */
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof FileEntry) {
+ FileEntry entry = (FileEntry)inputElement;
+ if (entry.isRoot()) {
+ return getChildren(mRootEntry);
+ }
+ }
+
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IContentProvider#dispose()
+ */
+ @Override
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ if (viewer instanceof TreeViewer) {
+ mViewer = (TreeViewer)viewer;
+ }
+ if (newInput instanceof FileEntry) {
+ mRootEntry = (FileEntry)newInput;
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java
new file mode 100644
index 0000000..b69d3b5
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java
@@ -0,0 +1,922 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.Panel;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.FileTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Device filesystem explorer class.
+ */
+public class DeviceExplorer extends Panel {
+
+ private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
+ private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
+
+ private static Pattern mKeyFilePattern = Pattern.compile(
+ "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
+ private static Pattern mDataFilePattern = Pattern.compile(
+ "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
+
+ public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
+ public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
+ public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
+ public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
+ public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
+ public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
+
+ private Composite mParent;
+ private TreeViewer mTreeViewer;
+ private Tree mTree;
+ private DeviceContentProvider mContentProvider;
+
+ private ICommonAction mPushAction;
+ private ICommonAction mPullAction;
+ private ICommonAction mDeleteAction;
+ private ICommonAction mCreateNewFolderAction;
+
+ private Image mFileImage;
+ private Image mFolderImage;
+ private Image mPackageImage;
+ private Image mOtherImage;
+
+ private IDevice mCurrentDevice;
+
+ private String mDefaultSave;
+
+ public DeviceExplorer() {
+ }
+
+ /**
+ * Sets custom images for the device explorer. If none are set then defaults are used.
+ * This can be useful to set platform-specific explorer icons.
+ *
+ * This should be called before {@link #createControl(Composite)}.
+ *
+ * @param fileImage the icon to represent a file.
+ * @param folderImage the icon to represent a folder.
+ * @param packageImage the icon to represent an apk.
+ * @param otherImage the icon to represent other types of files.
+ */
+ public void setCustomImages(Image fileImage, Image folderImage, Image packageImage,
+ Image otherImage) {
+ mFileImage = fileImage;
+ mFolderImage = folderImage;
+ mPackageImage = packageImage;
+ mOtherImage = otherImage;
+ }
+
+ /**
+ * Sets the actions so that the device explorer can enable/disable them based on the current
+ * selection
+ * @param pushAction
+ * @param pullAction
+ * @param deleteAction
+ * @param createNewFolderAction
+ */
+ public void setActions(ICommonAction pushAction, ICommonAction pullAction,
+ ICommonAction deleteAction, ICommonAction createNewFolderAction) {
+ mPushAction = pushAction;
+ mPullAction = pullAction;
+ mDeleteAction = deleteAction;
+ mCreateNewFolderAction = createNewFolderAction;
+ }
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+ parent.setLayout(new FillLayout());
+
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+ if (mFileImage == null) {
+ mFileImage = loader.loadImage("file.png", mParent.getDisplay());
+ }
+ if (mFolderImage == null) {
+ mFolderImage = loader.loadImage("folder.png", mParent.getDisplay());
+ }
+ if (mPackageImage == null) {
+ mPackageImage = loader.loadImage("android.png", mParent.getDisplay());
+ }
+ if (mOtherImage == null) {
+ // TODO: find a default image for other.
+ }
+
+ mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
+ mTree.setHeaderVisible(true);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // create columns
+ TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+ "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
+ "000000", COLUMN_SIZE, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
+ "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
+ "20:54", COLUMN_TIME, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
+ "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
+ "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
+
+ // create the jface wrapper
+ mTreeViewer = new TreeViewer(mTree);
+
+ // setup data provider
+ mContentProvider = new DeviceContentProvider();
+ mTreeViewer.setContentProvider(mContentProvider);
+ mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
+ mFolderImage, mPackageImage, mOtherImage));
+
+ // setup a listener for selection
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection sel = event.getSelection();
+ if (sel.isEmpty()) {
+ mPullAction.setEnabled(false);
+ mPushAction.setEnabled(false);
+ mDeleteAction.setEnabled(false);
+ mCreateNewFolderAction.setEnabled(false);
+ return;
+ }
+ if (sel instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) sel;
+ Object element = selection.getFirstElement();
+ if (element == null)
+ return;
+ if (element instanceof FileEntry) {
+ mPullAction.setEnabled(true);
+ mPushAction.setEnabled(selection.size() == 1);
+ if (selection.size() == 1) {
+ FileEntry entry = (FileEntry) element;
+ setDeleteEnabledState(entry);
+ mCreateNewFolderAction.setEnabled(entry.isDirectory());
+ } else {
+ mDeleteAction.setEnabled(false);
+ }
+ }
+ }
+ }
+ });
+
+ // add support for double click
+ mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ ISelection sel = event.getSelection();
+
+ if (sel instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) sel;
+
+ if (selection.size() == 1) {
+ FileEntry entry = (FileEntry)selection.getFirstElement();
+ String name = entry.getName();
+
+ FileEntry parentEntry = entry.getParent();
+
+ // can't really do anything with no parent
+ if (parentEntry == null) {
+ return;
+ }
+
+ // check this is a file like we want.
+ Matcher m = mKeyFilePattern.matcher(name);
+ if (m.matches()) {
+ // get the name w/o the extension
+ String baseName = m.group(1);
+
+ // add the data extension
+ String dataName = baseName + TRACE_DATA_EXT;
+
+ FileEntry dataEntry = parentEntry.findChild(dataName);
+
+ handleTraceDoubleClick(baseName, entry, dataEntry);
+
+ } else {
+ m = mDataFilePattern.matcher(name);
+ if (m.matches()) {
+ // get the name w/o the extension
+ String baseName = m.group(1);
+
+ // add the key extension
+ String keyName = baseName + TRACE_KEY_EXT;
+
+ FileEntry keyEntry = parentEntry.findChild(keyName);
+
+ handleTraceDoubleClick(baseName, keyEntry, entry);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // setup drop listener
+ mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
+ new Transfer[] { FileTransfer.getInstance() },
+ new ViewerDropAdapter(mTreeViewer) {
+ @Override
+ public boolean performDrop(Object data) {
+ // get the item on which we dropped the item(s)
+ FileEntry target = (FileEntry)getCurrentTarget();
+
+ // in case we drop at the same level as root
+ if (target == null) {
+ return false;
+ }
+
+ // if the target is not a directory, we get the parent directory
+ if (target.isDirectory() == false) {
+ target = target.getParent();
+ }
+
+ if (target == null) {
+ return false;
+ }
+
+ // get the list of files to drop
+ String[] files = (String[])data;
+
+ // do the drop
+ pushFiles(files, target);
+
+ // we need to finish with a refresh
+ refresh(target);
+
+ return true;
+ }
+
+ @Override
+ public boolean validateDrop(Object target, int operation, TransferData transferType) {
+ if (target == null) {
+ return false;
+ }
+
+ // convert to the real item
+ FileEntry targetEntry = (FileEntry)target;
+
+ // if the target is not a directory, we get the parent directory
+ if (targetEntry.isDirectory() == false) {
+ target = targetEntry.getParent();
+ }
+
+ if (target == null) {
+ return false;
+ }
+
+ return true;
+ }
+ });
+
+ // create and start the refresh thread
+ new Thread("Device Ls refresher") {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ sleep(FileListingService.REFRESH_RATE);
+ } catch (InterruptedException e) {
+ return;
+ }
+
+ if (mTree != null && mTree.isDisposed() == false) {
+ Display display = mTree.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ mTreeViewer.refresh(true);
+ }
+ }
+ });
+ } else {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ }
+ }.start();
+
+ return mTree;
+ }
+
+ @Override
+ protected void postCreation() {
+
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTree.setFocus();
+ }
+
+ /**
+ * Processes a double click on a trace file
+ * @param baseName the base name of the 2 files.
+ * @param keyEntry The FileEntry for the .key file.
+ * @param dataEntry The FileEntry for the .data file.
+ */
+ private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
+ FileEntry dataEntry) {
+ // first we need to download the files.
+ File keyFile;
+ File dataFile;
+ String path;
+ try {
+ // create a temp file for keyFile
+ File f = File.createTempFile(baseName, DdmConstants.DOT_TRACE);
+ f.delete();
+ f.mkdir();
+
+ path = f.getAbsolutePath();
+
+ keyFile = new File(path + File.separator + keyEntry.getName());
+ dataFile = new File(path + File.separator + dataEntry.getName());
+ } catch (IOException e) {
+ return;
+ }
+
+ // download the files
+ try {
+ SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
+ sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
+ sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
+
+ // now that we have the file, we need to launch traceview
+ String[] command = new String[2];
+ command[0] = DdmUiPreferences.getTraceview();
+ command[1] = path + File.separator + baseName;
+
+ try {
+ final Process p = Runtime.getRuntime().exec(command);
+
+ // create a thread for the output
+ new Thread("Traceview output") {
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(p.getErrorStream());
+ BufferedReader resultReader = new BufferedReader(is);
+
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ try {
+ while (true) {
+ String line = resultReader.readLine();
+ if (line != null) {
+ DdmConsole.printErrorToConsole("Traceview: " + line);
+ } else {
+ break;
+ }
+ }
+ // get the return code from the process
+ p.waitFor();
+ } catch (IOException e) {
+ } catch (InterruptedException e) {
+
+ }
+ }
+ }.start();
+
+ } catch (IOException e) {
+ }
+ }
+ } catch (IOException e) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+ return;
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+ return;
+ }
+ } catch (TimeoutException e) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: timeout", keyEntry.getName()));
+ } catch (AdbCommandRejectedException e) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+ }
+ }
+
+ /**
+ * Pull the current selection on the local drive. This method displays
+ * a dialog box to let the user select where to store the file(s) and
+ * folder(s).
+ */
+ public void pullSelection() {
+ // get the selection
+ TreeItem[] items = mTree.getSelection();
+
+ // name of the single file pull, or null if we're pulling a directory
+ // or more than one object.
+ String filePullName = null;
+ FileEntry singleEntry = null;
+
+ // are we pulling a single file?
+ if (items.length == 1) {
+ singleEntry = (FileEntry)items[0].getData();
+ if (singleEntry.getType() == FileListingService.TYPE_FILE) {
+ filePullName = singleEntry.getName();
+ }
+ }
+
+ // where do we save by default?
+ String defaultPath = mDefaultSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+
+ if (filePullName != null) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Get Device File");
+ fileDialog.setFileName(filePullName);
+ fileDialog.setFilterPath(defaultPath);
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ mDefaultSave = fileDialog.getFilterPath();
+
+ pullFile(singleEntry, fileName);
+ }
+ } else {
+ DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
+
+ directoryDialog.setText("Get Device Files/Folders");
+ directoryDialog.setFilterPath(defaultPath);
+
+ String directoryName = directoryDialog.open();
+ if (directoryName != null) {
+ pullSelection(items, directoryName);
+ }
+ }
+ }
+
+ /**
+ * Push new file(s) and folder(s) into the current selection. Current
+ * selection must be single item. If the current selection is not a
+ * directory, the parent directory is used.
+ * This method displays a dialog to let the user choose file to push to
+ * the device.
+ */
+ public void pushIntoSelection() {
+ // get the name of the object we're going to pull
+ TreeItem[] items = mTree.getSelection();
+
+ if (items.length == 0) {
+ return;
+ }
+
+ FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
+ String fileName;
+
+ dlg.setText("Put File on Device");
+
+ // There should be only one.
+ FileEntry entry = (FileEntry)items[0].getData();
+ dlg.setFileName(entry.getName());
+
+ String defaultPath = mDefaultSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+ dlg.setFilterPath(defaultPath);
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ mDefaultSave = dlg.getFilterPath();
+
+ // we need to figure out the remote path based on the current selection type.
+ String remotePath;
+ FileEntry toRefresh = entry;
+ if (entry.isDirectory()) {
+ remotePath = entry.getFullPath();
+ } else {
+ toRefresh = entry.getParent();
+ remotePath = toRefresh.getFullPath();
+ }
+
+ pushFile(fileName, remotePath);
+ mTreeViewer.refresh(toRefresh);
+ }
+ }
+
+ public void deleteSelection() {
+ // get the name of the object we're going to pull
+ TreeItem[] items = mTree.getSelection();
+
+ if (items.length != 1) {
+ return;
+ }
+
+ FileEntry entry = (FileEntry)items[0].getData();
+ final FileEntry parentEntry = entry.getParent();
+
+ // create the delete command
+ String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
+
+ try {
+ mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
+ @Override
+ public void addOutput(byte[] data, int offset, int length) {
+ // pass
+ // TODO get output to display errors if any.
+ }
+
+ @Override
+ public void flush() {
+ mTreeViewer.refresh(parentEntry);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ // adb failed somehow, we do nothing. We should be displaying the error from the output
+ // of the shell command.
+ } catch (TimeoutException e) {
+ // adb failed somehow, we do nothing. We should be displaying the error from the output
+ // of the shell command.
+ } catch (AdbCommandRejectedException e) {
+ // adb failed somehow, we do nothing. We should be displaying the error from the output
+ // of the shell command.
+ } catch (ShellCommandUnresponsiveException e) {
+ // adb failed somehow, we do nothing. We should be displaying the error from the output
+ // of the shell command.
+ }
+
+ }
+
+ public void createNewFolderInSelection() {
+ TreeItem[] items = mTree.getSelection();
+
+ if (items.length != 1) {
+ return;
+ }
+
+ final FileEntry entry = (FileEntry) items[0].getData();
+
+ if (entry.isDirectory()) {
+ InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder",
+ "Please enter the new folder name", "New Folder", new IInputValidator() {
+ @Override
+ public String isValid(String newText) {
+ if ((newText != null) && (newText.length() > 0)
+ && (newText.trim().length() > 0)
+ && (newText.indexOf('/') == -1)
+ && (newText.indexOf('\\') == -1)) {
+ return null;
+ } else {
+ return "Invalid name";
+ }
+ }
+ });
+ inputDialog.open();
+ String value = inputDialog.getValue();
+
+ if (value != null) {
+ // create the mkdir command
+ String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$
+ + FileListingService.FILE_SEPARATOR + FileEntry.escape(value);
+
+ try {
+ mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void flush() {
+ mTreeViewer.refresh(entry);
+ }
+
+ @Override
+ public void addOutput(byte[] data, int offset, int length) {
+ String errorMessage;
+ if (data != null) {
+ errorMessage = new String(data);
+ } else {
+ errorMessage = "";
+ }
+ Status status = new Status(IStatus.ERROR,
+ "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$
+ ErrorDialog.openError(mTree.getShell(), "New Folder Error",
+ "New Folder Error", status);
+ }
+ });
+ } catch (TimeoutException e) {
+ // adb failed somehow, we do nothing. We should be
+ // displaying the error from the output of the shell
+ // command.
+ } catch (AdbCommandRejectedException e) {
+ // adb failed somehow, we do nothing. We should be
+ // displaying the error from the output of the shell
+ // command.
+ } catch (ShellCommandUnresponsiveException e) {
+ // adb failed somehow, we do nothing. We should be
+ // displaying the error from the output of the shell
+ // command.
+ } catch (IOException e) {
+ // adb failed somehow, we do nothing. We should be
+ // displaying the error from the output of the shell
+ // command.
+ }
+ }
+ }
+ }
+
+ /**
+ * Force a full refresh of the explorer.
+ */
+ public void refresh() {
+ mTreeViewer.refresh(true);
+ }
+
+ /**
+ * Sets the new device to explorer
+ */
+ public void switchDevice(final IDevice device) {
+ if (device != mCurrentDevice) {
+ mCurrentDevice = device;
+ // now we change the input. but we need to do that in the
+ // ui thread.
+ if (mTree.isDisposed() == false) {
+ Display d = mTree.getDisplay();
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // new service
+ if (mCurrentDevice != null) {
+ FileListingService fls = mCurrentDevice.getFileListingService();
+ mContentProvider.setListingService(fls);
+ mTreeViewer.setInput(fls.getRoot());
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Refresh an entry from a non ui thread.
+ * @param entry the entry to refresh.
+ */
+ private void refresh(final FileEntry entry) {
+ Display d = mTreeViewer.getTree().getDisplay();
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mTreeViewer.refresh(entry);
+ }
+ });
+ }
+
+ /**
+ * Pulls the selection from a device.
+ * @param items the tree selection the remote file on the device
+ * @param localDirector the local directory in which to save the files.
+ */
+ private void pullSelection(TreeItem[] items, final String localDirectory) {
+ try {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ // make a list of the FileEntry.
+ ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
+ for (TreeItem item : items) {
+ Object data = item.getData();
+ if (data instanceof FileEntry) {
+ entries.add((FileEntry)data);
+ }
+ }
+ final FileEntry[] entryArray = entries.toArray(
+ new FileEntry[entries.size()]);
+
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor)
+ throws SyncException, IOException, TimeoutException {
+ sync.pull(entryArray, localDirectory, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ }, "Pulling file(s) from the device", mParent.getShell());
+ }
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull selection: %1$s", e.getMessage()));
+ }
+ } catch (Exception e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+
+ /**
+ * Pulls a file from a device.
+ * @param remote the remote file on the device
+ * @param local the destination filepath
+ */
+ private void pullFile(final FileEntry remote, final String local) {
+ try {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor)
+ throws SyncException, IOException, TimeoutException {
+ sync.pullFile(remote, local, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ }, String.format("Pulling %1$s from the device", remote.getName()),
+ mParent.getShell());
+ }
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull selection: %1$s", e.getMessage()));
+ }
+ } catch (Exception e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+
+ /**
+ * Pushes several files and directory into a remote directory.
+ * @param localFiles
+ * @param remoteDirectory
+ */
+ private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
+ try {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor)
+ throws SyncException, IOException, TimeoutException {
+ sync.push(localFiles, remoteDirectory, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ }, "Pushing file(s) to the device", mParent.getShell());
+ }
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push selection: %1$s", e.getMessage()));
+ }
+ } catch (Exception e) {
+ DdmConsole.printErrorToConsole("Failed to push the items");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+
+ /**
+ * Pushes a file on a device.
+ * @param local the local filepath of the file to push
+ * @param remoteDirectory the remote destination directory on the device
+ */
+ private void pushFile(final String local, final String remoteDirectory) {
+ try {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ // get the file name
+ String[] segs = local.split(Pattern.quote(File.separator));
+ String name = segs[segs.length-1];
+ final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
+ + name;
+
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor)
+ throws SyncException, IOException, TimeoutException {
+ sync.pushFile(local, remoteFile, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ }, String.format("Pushing %1$s to the device.", name), mParent.getShell());
+ }
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push selection: %1$s", e.getMessage()));
+ }
+ } catch (Exception e) {
+ DdmConsole.printErrorToConsole("Failed to push the item(s).");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+
+ /**
+ * Sets the enabled state based on a FileEntry properties
+ * @param element The selected FileEntry
+ */
+ protected void setDeleteEnabledState(FileEntry element) {
+ mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java
new file mode 100644
index 0000000..1240e59
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Label provider for the FileEntry.
+ */
+class FileLabelProvider implements ILabelProvider, ITableLabelProvider {
+
+ private Image mFileImage;
+ private Image mFolderImage;
+ private Image mPackageImage;
+ private Image mOtherImage;
+
+ /**
+ * Creates Label provider with custom images.
+ * @param fileImage the Image to represent a file
+ * @param folderImage the Image to represent a folder
+ * @param packageImage the Image to represent a .apk file. If null,
+ * fileImage is used instead.
+ * @param otherImage the Image to represent all other entry type.
+ */
+ public FileLabelProvider(Image fileImage, Image folderImage,
+ Image packageImage, Image otherImage) {
+ mFileImage = fileImage;
+ mFolderImage = folderImage;
+ mOtherImage = otherImage;
+ if (packageImage != null) {
+ mPackageImage = packageImage;
+ } else {
+ mPackageImage = fileImage;
+ }
+ }
+
+ /**
+ * Creates a label provider with default images.
+ *
+ */
+ public FileLabelProvider() {
+
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
+ */
+ @Override
+ public Image getImage(Object element) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
+ */
+ @Override
+ public String getText(Object element) {
+ return null;
+ }
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == 0) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+ switch (entry.getType()) {
+ case FileListingService.TYPE_FILE:
+ case FileListingService.TYPE_LINK:
+ // get the name and extension
+ if (entry.isApplicationPackage()) {
+ return mPackageImage;
+ }
+ return mFileImage;
+ case FileListingService.TYPE_DIRECTORY:
+ case FileListingService.TYPE_DIRECTORY_LINK:
+ return mFolderImage;
+ }
+ }
+
+ // default case return a different image.
+ return mOtherImage;
+ }
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ switch (columnIndex) {
+ case 0:
+ return entry.getName();
+ case 1:
+ return entry.getSize();
+ case 2:
+ return entry.getDate();
+ case 3:
+ return entry.getTime();
+ case 4:
+ return entry.getPermissions();
+ case 5:
+ return entry.getInfo();
+ }
+ }
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener)
+ */
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // we don't need listeners.
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
+ */
+ @Override
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
+ */
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ return false;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
+ */
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // we don't need listeners
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java
new file mode 100644
index 0000000..f50a94c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.handler;
+
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+import com.android.ddmlib.ClientData.IMethodProfilingHandler;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Base handler class for handler dealing with files located on a device.
+ *
+ * @see IHprofDumpHandler
+ * @see IMethodProfilingHandler
+ */
+public abstract class BaseFileHandler {
+
+ protected final Shell mParentShell;
+
+ public BaseFileHandler(Shell parentShell) {
+ mParentShell = parentShell;
+ }
+
+ protected abstract String getDialogTitle();
+
+ /**
+ * Prompts the user for a save location and pulls the remote files into this location.
+ * <p/>This <strong>must</strong> be called from the UI Thread.
+ * @param sync the {@link SyncService} to use to pull the file from the device
+ * @param localFileName The default local name
+ * @param remoteFilePath The name of the file to pull off of the device
+ * @param title The title of the File Save dialog.
+ * @return The result of the pull as a {@link SyncResult} object, or null if the sync
+ * didn't happen (canceled by the user).
+ * @throws InvocationTargetException
+ * @throws InterruptedException
+ * @throws SyncException if an error happens during the push of the package on the device.
+ * @throws IOException
+ */
+ protected void promptAndPull(final SyncService sync,
+ String localFileName, final String remoteFilePath, String title)
+ throws InvocationTargetException, InterruptedException, SyncException, TimeoutException,
+ IOException {
+ FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE);
+
+ fileDialog.setText(title);
+ fileDialog.setFileName(localFileName);
+
+ final String localFilePath = fileDialog.open();
+ if (localFilePath != null) {
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor) throws SyncException, IOException,
+ TimeoutException {
+ sync.pullFile(remoteFilePath, localFilePath, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ },
+ String.format("Pulling %1$s from the device", remoteFilePath), mParentShell);
+ }
+ }
+
+ /**
+ * Prompts the user for a save location and copies a temp file into it.
+ * <p/>This <strong>must</strong> be called from the UI Thread.
+ * @param localFileName The default local name
+ * @param tempFilePath The name of the temp file to copy.
+ * @param title The title of the File Save dialog.
+ * @return true if success, false on error or cancel.
+ */
+ protected boolean promptAndSave(String localFileName, byte[] data, String title) {
+ FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE);
+
+ fileDialog.setText(title);
+ fileDialog.setFileName(localFileName);
+
+ String localFilePath = fileDialog.open();
+ if (localFilePath != null) {
+ try {
+ saveFile(data, new File(localFilePath));
+ return true;
+ } catch (IOException e) {
+ String errorMsg = e.getMessage();
+ displayErrorInUiThread(
+ "Failed to save file '%1$s'%2$s",
+ localFilePath,
+ errorMsg != null ? ":\n" + errorMsg : ".");
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Display an error message.
+ * <p/>This will call about to {@link Display} to run this in an async {@link Runnable} in the
+ * UI Thread. This is safe to be called from a non-UI Thread.
+ * @param format the string to display
+ * @param args the string arguments
+ */
+ protected void displayErrorInUiThread(final String format, final Object... args) {
+ mParentShell.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ MessageDialog.openError(mParentShell, getDialogTitle(),
+ String.format(format, args));
+ }
+ });
+ }
+
+ /**
+ * Display an error message.
+ * This must be called from the UI Thread.
+ * @param format the string to display
+ * @param args the string arguments
+ */
+ protected void displayErrorFromUiThread(final String format, final Object... args) {
+ MessageDialog.openError(mParentShell, getDialogTitle(),
+ String.format(format, args));
+ }
+
+ /**
+ * Saves a given data into a temp file and returns its corresponding {@link File} object.
+ * @param data the data to save
+ * @return the File into which the data was written or null if it failed.
+ * @throws IOException
+ */
+ protected File saveTempFile(byte[] data, String extension) throws IOException {
+ File f = File.createTempFile("ddms", extension);
+ saveFile(data, f);
+ return f;
+ }
+
+ /**
+ * Saves some data into a given File.
+ * @param data the data to save
+ * @param output the file into the data is saved.
+ * @throws IOException
+ */
+ protected void saveFile(byte[] data, File output) throws IOException {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(output);
+ fos.write(data);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java
new file mode 100644
index 0000000..ab1b5f7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.handler;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData.IMethodProfilingHandler;
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Handler for Method tracing.
+ * This will pull the trace file into a temp file and launch traceview.
+ */
+public class MethodProfilingHandler extends BaseFileHandler
+ implements IMethodProfilingHandler {
+
+ public MethodProfilingHandler(Shell parentShell) {
+ super(parentShell);
+ }
+
+ @Override
+ protected String getDialogTitle() {
+ return "Method Profiling Error";
+ }
+
+ @Override
+ public void onStartFailure(final Client client, final String message) {
+ displayErrorInUiThread(
+ "Unable to create Method Profiling file for application '%1$s'\n\n%2$s" +
+ "Check logcat for more information.",
+ client.getClientData().getClientDescription(),
+ message != null ? message + "\n\n" : "");
+ }
+
+ @Override
+ public void onEndFailure(final Client client, final String message) {
+ displayErrorInUiThread(
+ "Unable to finish Method Profiling for application '%1$s'\n\n%2$s" +
+ "Check logcat for more information.",
+ client.getClientData().getClientDescription(),
+ message != null ? message + "\n\n" : "");
+ }
+
+ @Override
+ public void onSuccess(final String remoteFilePath, final Client client) {
+ mParentShell.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (remoteFilePath == null) {
+ displayErrorFromUiThread(
+ "Unable to download trace file: unknown file name.\n" +
+ "This can happen if you disconnected the device while recording the trace.");
+ return;
+ }
+
+ final IDevice device = client.getDevice();
+ try {
+ // get the sync service to pull the HPROF file
+ final SyncService sync = client.getDevice().getSyncService();
+ if (sync != null) {
+ pullAndOpen(sync, remoteFilePath);
+ } else {
+ displayErrorFromUiThread(
+ "Unable to download trace file from device '%1$s'.",
+ device.getSerialNumber());
+ }
+ } catch (Exception e) {
+ displayErrorFromUiThread("Unable to download trace file from device '%1$s'.",
+ device.getSerialNumber());
+ }
+ }
+
+ });
+ }
+
+ @Override
+ public void onSuccess(byte[] data, final Client client) {
+ try {
+ File tempFile = saveTempFile(data, DdmConstants.DOT_TRACE);
+ open(tempFile.getAbsolutePath());
+ } catch (IOException e) {
+ String errorMsg = e.getMessage();
+ displayErrorInUiThread(
+ "Failed to save trace data into temp file%1$s",
+ errorMsg != null ? ":\n" + errorMsg : ".");
+ }
+ }
+
+ /**
+ * pulls and open a file. This is run from the UI thread.
+ */
+ private void pullAndOpen(final SyncService sync, final String remoteFilePath)
+ throws InvocationTargetException, InterruptedException, IOException {
+ // get a temp file
+ File temp = File.createTempFile("android", DdmConstants.DOT_TRACE); //$NON-NLS-1$
+ final String tempPath = temp.getAbsolutePath();
+
+ // pull the file
+ try {
+ SyncProgressHelper.run(new SyncRunnable() {
+ @Override
+ public void run(ISyncProgressMonitor monitor)
+ throws SyncException, IOException, TimeoutException {
+ sync.pullFile(remoteFilePath, tempPath, monitor);
+ }
+
+ @Override
+ public void close() {
+ sync.close();
+ }
+ },
+ String.format("Pulling %1$s from the device", remoteFilePath), mParentShell);
+
+ // open the temp file in traceview
+ open(tempPath);
+ } catch (SyncException e) {
+ if (e.wasCanceled() == false) {
+ displayErrorFromUiThread("Unable to download trace file:\n\n%1$s", e.getMessage());
+ }
+ } catch (TimeoutException e) {
+ displayErrorFromUiThread("Unable to download trace file:\n\ntimeout");
+ }
+ }
+
+ protected void open(String tempPath) {
+ // now that we have the file, we need to launch traceview
+ String[] command = new String[2];
+ command[0] = DdmUiPreferences.getTraceview();
+ command[1] = tempPath;
+
+ try {
+ final Process p = Runtime.getRuntime().exec(command);
+
+ // create a thread for the output
+ new Thread("Traceview output") {
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(p.getErrorStream());
+ BufferedReader resultReader = new BufferedReader(is);
+
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ try {
+ while (true) {
+ String line = resultReader.readLine();
+ if (line != null) {
+ DdmConsole.printErrorToConsole("Traceview: " + line);
+ } else {
+ break;
+ }
+ }
+ // get the return code from the process
+ p.waitFor();
+ } catch (Exception e) {
+ Log.e("traceview", e);
+ }
+ }
+ }.start();
+ } catch (IOException e) {
+ Log.e("traceview", e);
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java
new file mode 100644
index 0000000..88db5cc
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.InputMismatchException;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public class NativeHeapDataImporter implements IRunnableWithProgress {
+ private LineNumberReader mReader;
+ private int mStartLineNumber;
+ private int mEndLineNumber;
+
+ private NativeHeapSnapshot mSnapshot;
+
+ public NativeHeapDataImporter(Reader stream) {
+ mReader = new LineNumberReader(stream);
+ mReader.setLineNumber(1); // start numbering at 1
+ }
+
+ @Override
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException, InterruptedException {
+ monitor.beginTask("Importing Heap Data", IProgressMonitor.UNKNOWN);
+
+ List<NativeAllocationInfo> allocations = new ArrayList<NativeAllocationInfo>();
+ try {
+ while (true) {
+ String line;
+ StringBuilder sb = new StringBuilder();
+
+ // read in a sequence of lines corresponding to a single NativeAllocationInfo
+ mStartLineNumber = mReader.getLineNumber();
+ while ((line = mReader.readLine()) != null) {
+ if (line.trim().length() == 0) {
+ // each block of allocations end with an empty line
+ break;
+ }
+
+ sb.append(line);
+ sb.append('\n');
+ }
+ mEndLineNumber = mReader.getLineNumber();
+
+ // parse those lines into a NativeAllocationInfo object
+ String allocationBlock = sb.toString();
+ if (allocationBlock.trim().length() > 0) {
+ allocations.add(getNativeAllocation(allocationBlock));
+ }
+
+ if (line == null) { // EOF
+ break;
+ }
+ }
+ } catch (Exception e) {
+ if (e.getMessage() == null) {
+ e = new RuntimeException(genericErrorMessage("Unexpected Parse error"));
+ }
+ throw new InvocationTargetException(e);
+ } finally {
+ try {
+ mReader.close();
+ } catch (IOException e) {
+ // we can ignore this exception
+ }
+ monitor.done();
+ }
+
+ mSnapshot = new NativeHeapSnapshot(allocations);
+ }
+
+ /** Parse a single native allocation dump. This is the complement of
+ * {@link NativeAllocationInfo#toString()}.
+ *
+ * An allocation is of the following form:
+ * Allocations: 1
+ * Size: 344748
+ * Total Size: 344748
+ * BeginStackTrace:
+ * 40069bd8 /lib/libc_malloc_leak.so --- get_backtrace --- /libc/bionic/malloc_leak.c:258
+ * 40069dd8 /lib/libc_malloc_leak.so --- leak_calloc --- /libc/bionic/malloc_leak.c:576
+ * 40069bd8 /lib/libc_malloc_leak.so --- 40069bd8 ---
+ * 40069dd8 /lib/libc_malloc_leak.so --- 40069dd8 ---
+ * EndStackTrace
+ * Note that in the above stack trace, the last two lines are examples where the address
+ * was not resolved.
+ *
+ * @param block a string of lines corresponding to a single {@code NativeAllocationInfo}
+ * @return parse the input and return the corresponding {@link NativeAllocationInfo}
+ * @throws InputMismatchException if there are any parse errors
+ */
+ private NativeAllocationInfo getNativeAllocation(String block) {
+ Scanner sc = new Scanner(block);
+
+ String kw = sc.next();
+ if (!NativeAllocationInfo.ALLOCATIONS_KW.equals(kw)) {
+ throw new InputMismatchException(
+ expectedKeywordErrorMessage(NativeAllocationInfo.ALLOCATIONS_KW, kw));
+ }
+
+ int allocations = sc.nextInt();
+
+ kw = sc.next();
+ if (!NativeAllocationInfo.SIZE_KW.equals(kw)) {
+ throw new InputMismatchException(
+ expectedKeywordErrorMessage(NativeAllocationInfo.SIZE_KW, kw));
+ }
+
+ int size = sc.nextInt();
+
+ kw = sc.next();
+ if (!NativeAllocationInfo.TOTAL_SIZE_KW.equals(kw)) {
+ throw new InputMismatchException(
+ expectedKeywordErrorMessage(NativeAllocationInfo.TOTAL_SIZE_KW, kw));
+ }
+
+ int totalSize = sc.nextInt();
+ if (totalSize != size * allocations) {
+ throw new InputMismatchException(
+ genericErrorMessage("Total Size does not match size * # of allocations"));
+ }
+
+ NativeAllocationInfo info = new NativeAllocationInfo(size, allocations);
+
+ kw = sc.next();
+ if (!NativeAllocationInfo.BEGIN_STACKTRACE_KW.equals(kw)) {
+ throw new InputMismatchException(
+ expectedKeywordErrorMessage(NativeAllocationInfo.BEGIN_STACKTRACE_KW, kw));
+ }
+
+ List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>();
+ Pattern endTracePattern = Pattern.compile(NativeAllocationInfo.END_STACKTRACE_KW);
+
+ while (true) {
+ long address = sc.nextLong(16);
+ info.addStackCallAddress(address);
+
+ String library = sc.next();
+ sc.next(); // ignore "---"
+ String method = scanTillSeparator(sc, "---");
+
+ String filename = "";
+ if (!isUnresolved(method, address)) {
+ filename = sc.next();
+ }
+
+ stackInfo.add(new NativeStackCallInfo(address, library, method, filename));
+
+ if (sc.hasNext(endTracePattern)) {
+ break;
+ }
+ }
+
+ info.setResolvedStackCall(stackInfo);
+ return info;
+ }
+
+ private String scanTillSeparator(Scanner sc, String separator) {
+ StringBuilder sb = new StringBuilder();
+
+ while (true) {
+ String token = sc.next();
+ if (token.equals(separator)) {
+ break;
+ }
+
+ sb.append(token);
+
+ // We do not know the exact delimiter that was skipped over, but we know
+ // that there was atleast 1 whitespace. Add a single whitespace character
+ // to account for this.
+ sb.append(' ');
+ }
+
+ return sb.toString().trim();
+ }
+
+ private boolean isUnresolved(String method, long address) {
+ // a method is unresolved if it is just the hex representation of the address
+ return Long.toString(address, 16).equals(method);
+ }
+
+ private String genericErrorMessage(String message) {
+ return String.format("%1$s between lines %2$d and %3$d",
+ message, mStartLineNumber, mEndLineNumber);
+ }
+
+ private String expectedKeywordErrorMessage(String expected, String actual) {
+ return String.format("Expected keyword '%1$s', saw '%2$s' between lines %3$d to %4$d.",
+ expected, actual, mStartLineNumber, mEndLineNumber);
+ }
+
+ public NativeHeapSnapshot getImportedSnapshot() {
+ return mSnapshot;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java
new file mode 100644
index 0000000..9eb6ddf
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Models a heap snapshot that is the difference between two snapshots.
+ */
+public class NativeHeapDiffSnapshot extends NativeHeapSnapshot {
+ private long mCommonAllocationsTotalMemory;
+
+ public NativeHeapDiffSnapshot(NativeHeapSnapshot newSnapshot, NativeHeapSnapshot oldSnapshot) {
+ // The diff snapshots behaves like a snapshot that only contains the new allocations
+ // not present in the old snapshot
+ super(getNewAllocations(newSnapshot, oldSnapshot));
+
+ Set<NativeAllocationInfo> commonAllocations =
+ new HashSet<NativeAllocationInfo>(oldSnapshot.getAllocations());
+ commonAllocations.retainAll(newSnapshot.getAllocations());
+
+ // Memory common between the old and new snapshots
+ mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations);
+ }
+
+ private static List<NativeAllocationInfo> getNewAllocations(NativeHeapSnapshot newSnapshot,
+ NativeHeapSnapshot oldSnapshot) {
+ Set<NativeAllocationInfo> allocations =
+ new HashSet<NativeAllocationInfo>(newSnapshot.getAllocations());
+ allocations.removeAll(oldSnapshot.getAllocations());
+ return new ArrayList<NativeAllocationInfo>(allocations);
+ }
+
+ @Override
+ public String getFormattedMemorySize() {
+ // for a diff snapshot, we report the following string for display:
+ // xxx bytes new allocation + yyy bytes retained from previous allocation
+ // = zzz bytes total
+
+ long newAllocations = getTotalSize();
+ return String.format("%s bytes new + %s bytes retained = %s bytes total",
+ formatMemorySize(newAllocations),
+ formatMemorySize(mCommonAllocationsTotalMemory),
+ formatMemorySize(newAllocations + mCommonAllocationsTotalMemory));
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java
new file mode 100644
index 0000000..b96fa02
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * A Label Provider for the Native Heap TreeViewer in {@link NativeHeapPanel}.
+ */
+public class NativeHeapLabelProvider extends LabelProvider implements ITableLabelProvider {
+ private long mTotalSize;
+
+ @Override
+ public Image getColumnImage(Object arg0, int arg1) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int index) {
+ if (element instanceof NativeAllocationInfo) {
+ return getColumnTextForNativeAllocation((NativeAllocationInfo) element, index);
+ }
+
+ if (element instanceof NativeLibraryAllocationInfo) {
+ return getColumnTextForNativeLibrary((NativeLibraryAllocationInfo) element, index);
+ }
+
+ return null;
+ }
+
+ private String getColumnTextForNativeAllocation(NativeAllocationInfo info, int index) {
+ NativeStackCallInfo stackInfo = info.getRelevantStackCallInfo();
+
+ switch (index) {
+ case 0:
+ return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getLibraryName();
+ case 1:
+ return Integer.toString(info.getSize() * info.getAllocationCount());
+ case 2:
+ return getPercentageString(info.getSize() * info.getAllocationCount(), mTotalSize);
+ case 3:
+ String prefix = "";
+ if (!info.isZygoteChild()) {
+ prefix = "Z ";
+ }
+ return prefix + Integer.toString(info.getAllocationCount());
+ case 4:
+ return Integer.toString(info.getSize());
+ case 5:
+ return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getMethodName();
+ default:
+ return null;
+ }
+ }
+
+ private String getColumnTextForNativeLibrary(NativeLibraryAllocationInfo info, int index) {
+ switch (index) {
+ case 0:
+ return info.getLibraryName();
+ case 1:
+ return Long.toString(info.getTotalSize());
+ case 2:
+ return getPercentageString(info.getTotalSize(), mTotalSize);
+ default:
+ return null;
+ }
+ }
+
+ private String getPercentageString(long size, long total) {
+ if (total == 0) {
+ return "";
+ }
+
+ return String.format("%.1f%%", (float)(size * 100)/(float)total);
+ }
+
+ private String stackResolutionStatus(NativeAllocationInfo info) {
+ if (info.isStackCallResolved()) {
+ return "?"; // resolved and unknown
+ } else {
+ return "Resolving..."; // still resolving...
+ }
+ }
+
+ /**
+ * Set the total size of the heap dump for use in percentage calculations.
+ * This value should be set whenever the input to the tree changes so that the percentages
+ * are computed correctly.
+ */
+ public void setTotalSize(long totalSize) {
+ mTotalSize = totalSize;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
new file mode 100644
index 0000000..f6631b7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
@@ -0,0 +1,1150 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.Addr2Line;
+import com.android.ddmuilib.BaseHeapPanel;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Panel to display native heap information. */
+public class NativeHeapPanel extends BaseHeapPanel {
+ private static final boolean USE_OLD_RESOLVER;
+ static {
+ String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER");
+ if (useOldResolver != null && useOldResolver.equalsIgnoreCase("true")) {
+ USE_OLD_RESOLVER = true;
+ } else {
+ USE_OLD_RESOLVER = false;
+ }
+ }
+ private final int MAX_DISPLAYED_ERROR_ITEMS = 5;
+
+ private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data";
+ private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations";
+ private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot";
+ private static final String TOOLTIP_GROUPBY = "Group allocations by library.";
+
+ private static final String EXPORT_DATA_IMAGE = "save.png";
+ private static final String ZYGOTE_IMAGE = "zygote.png";
+ private static final String DIFFS_ONLY_IMAGE = "diff.png";
+ private static final String GROUPBY_IMAGE = "groupby.png";
+
+ private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage";
+ private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data";
+ private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:";
+ private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE =
+ "List of colon separated paths to search for symbol debug information. See tooltip for examples.";
+ private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT =
+ "Colon separated paths that contain unstripped libraries with debug symbols.\n"
+ + "e.g.: <android-src>/out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi";
+
+ private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only";
+ private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote";
+ private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library";
+ private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path";
+ private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent";
+ private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path";
+ private IPreferenceStore mPrefStore;
+
+ private List<NativeHeapSnapshot> mNativeHeapSnapshots;
+
+ // Maintain the differences between a snapshot and its predecessor.
+ // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1]
+ // The zeroth entry is null since there is no predecessor.
+ // The list is filled lazily on demand.
+ private List<NativeHeapSnapshot> mDiffSnapshots;
+
+ private Map<Integer, List<NativeHeapSnapshot>> mImportedSnapshotsPerPid;
+
+ private Button mSnapshotHeapButton;
+ private Button mLoadHeapDataButton;
+ private Text mSymbolSearchPathText;
+ private Combo mSnapshotIndexCombo;
+ private Label mMemoryAllocatedText;
+
+ private TreeViewer mDetailsTreeViewer;
+ private TreeViewer mStackTraceTreeViewer;
+ private NativeHeapProviderByAllocations mContentProviderByAllocations;
+ private NativeHeapProviderByLibrary mContentProviderByLibrary;
+ private NativeHeapLabelProvider mDetailsTreeLabelProvider;
+
+ private ToolBar mDetailsToolBar;
+ private ToolItem mGroupByButton;
+ private ToolItem mDiffsOnlyButton;
+ private ToolItem mShowZygoteAllocationsButton;
+ private ToolItem mExportHeapDataButton;
+
+ public NativeHeapPanel(IPreferenceStore prefStore) {
+ mPrefStore = prefStore;
+ mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75);
+ mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, "");
+ mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false);
+ mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true);
+ mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false);
+
+ mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>();
+ mDiffSnapshots = new ArrayList<NativeHeapSnapshot>();
+ mImportedSnapshotsPerPid = new HashMap<Integer, List<NativeHeapSnapshot>>();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ if (client != getCurrentClient()) {
+ return;
+ }
+
+ if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) {
+ return;
+ }
+
+ List<NativeAllocationInfo> allocations = client.getClientData().getNativeAllocationList();
+ if (allocations.size() == 0) {
+ return;
+ }
+
+ // We need to clone this list since getClientData().getNativeAllocationList() clobbers
+ // the list on future updates
+ final List<NativeAllocationInfo> nativeAllocations = shallowCloneList(allocations);
+
+ addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations));
+ updateDisplay();
+
+ // Attempt to resolve symbols in a separate thread.
+ // The UI should be refreshed once the symbols have been resolved.
+ if (USE_OLD_RESOLVER) {
+ Thread t = new Thread(new SymbolResolverTask(nativeAllocations,
+ client.getClientData().getMappedNativeLibraries()));
+ t.setName("Address to Symbol Resolver");
+ t.start();
+ } else {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ resolveSymbols();
+ mDetailsTreeViewer.refresh();
+ mStackTraceTreeViewer.refresh();
+ }
+
+ public void resolveSymbols() {
+ Shell shell = Display.getDefault().getActiveShell();
+ ProgressMonitorDialog d = new ProgressMonitorDialog(shell);
+
+ NativeSymbolResolverTask resolver = new NativeSymbolResolverTask(
+ nativeAllocations,
+ client.getClientData().getMappedNativeLibraries(),
+ mSymbolSearchPathText.getText());
+
+ try {
+ d.run(true, true, resolver);
+ } catch (InvocationTargetException e) {
+ MessageDialog.openError(shell,
+ "Error Resolving Symbols",
+ e.getCause().getMessage());
+ return;
+ } catch (InterruptedException e) {
+ return;
+ }
+
+ MessageDialog.openInformation(shell, "Symbol Resolution Status",
+ getResolutionStatusMessage(resolver));
+ }
+ });
+ }
+ }
+
+ private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Symbol Resolution Complete.\n\n");
+
+ // show addresses that were not mapped
+ Set<Long> unmappedAddresses = resolver.getUnmappedAddresses();
+ if (unmappedAddresses.size() > 0) {
+ sb.append(String.format("Unmapped addresses (%d): ",
+ unmappedAddresses.size()));
+ sb.append(getSampleForDisplay(unmappedAddresses));
+ sb.append('\n');
+ }
+
+ // show libraries that were not present on disk
+ Set<String> notFoundLibraries = resolver.getNotFoundLibraries();
+ if (notFoundLibraries.size() > 0) {
+ sb.append(String.format("Libraries not found on disk (%d): ",
+ notFoundLibraries.size()));
+ sb.append(getSampleForDisplay(notFoundLibraries));
+ sb.append('\n');
+ }
+
+ // show addresses that were mapped but not resolved
+ Set<Long> unresolvableAddresses = resolver.getUnresolvableAddresses();
+ if (unresolvableAddresses.size() > 0) {
+ sb.append(String.format("Unresolved addresses (%d): ",
+ unresolvableAddresses.size()));
+ sb.append(getSampleForDisplay(unresolvableAddresses));
+ sb.append('\n');
+ }
+
+ if (resolver.getAddr2LineErrorMessage() != null) {
+ sb.append("Error launching addr2line: ");
+ sb.append(resolver.getAddr2LineErrorMessage());
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Get the string representation for a collection of items.
+ * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first
+ * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account,
+ * and an ellipsis is added at the end.
+ */
+ private String getSampleForDisplay(Collection<?> items) {
+ StringBuilder sb = new StringBuilder();
+
+ int c = 1;
+ Iterator<?> it = items.iterator();
+ while (it.hasNext()) {
+ Object item = it.next();
+ if (item instanceof Long) {
+ sb.append(String.format("0x%x", item));
+ } else {
+ sb.append(item);
+ }
+
+ if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) {
+ sb.append(", ...");
+ break;
+ } else if (it.hasNext()) {
+ sb.append(", ");
+ }
+
+ c++;
+ }
+ return sb.toString();
+ }
+
+ private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) {
+ mNativeHeapSnapshots.add(snapshot);
+
+ // The diff snapshots are filled in lazily on demand.
+ // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null.
+ mDiffSnapshots.add(null);
+ }
+
+ private List<NativeAllocationInfo> shallowCloneList(List<NativeAllocationInfo> allocations) {
+ List<NativeAllocationInfo> clonedList =
+ new ArrayList<NativeAllocationInfo>(allocations.size());
+
+ for (NativeAllocationInfo i : allocations) {
+ clonedList.add(i);
+ }
+
+ return clonedList;
+ }
+
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ @Override
+ public void clientSelected() {
+ Client c = getCurrentClient();
+
+ if (c == null) {
+ // if there is no client selected, then we disable the buttons but leave the
+ // display as is so that whatever snapshots are displayed continue to stay
+ // visible to the user.
+ mSnapshotHeapButton.setEnabled(false);
+ mLoadHeapDataButton.setEnabled(false);
+ return;
+ }
+
+ mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>();
+ mDiffSnapshots = new ArrayList<NativeHeapSnapshot>();
+
+ mSnapshotHeapButton.setEnabled(true);
+ mLoadHeapDataButton.setEnabled(true);
+
+ List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(
+ c.getClientData().getPid());
+ if (importedSnapshots != null) {
+ for (NativeHeapSnapshot n : importedSnapshots) {
+ addNativeHeapSnapshot(n);
+ }
+ }
+
+ List<NativeAllocationInfo> allocations = c.getClientData().getNativeAllocationList();
+ allocations = shallowCloneList(allocations);
+
+ if (allocations.size() > 0) {
+ addNativeHeapSnapshot(new NativeHeapSnapshot(allocations));
+ }
+
+ updateDisplay();
+ }
+
+ private void updateDisplay() {
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateSnapshotIndexCombo();
+ updateToolbars();
+
+ int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1;
+ displaySnapshot(lastSnapshotIndex);
+ displayStackTraceForSelection();
+ }
+ });
+ }
+
+ private void displaySelectedSnapshot() {
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ int idx = mSnapshotIndexCombo.getSelectionIndex();
+ displaySnapshot(idx);
+ }
+ });
+ }
+
+ private void displaySnapshot(int index) {
+ if (index < 0 || mNativeHeapSnapshots.size() == 0) {
+ mDetailsTreeViewer.setInput(null);
+ mMemoryAllocatedText.setText("");
+ return;
+ }
+
+ assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index";
+
+ NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index);
+ if (mDiffsOnlyButton.getSelection() && index > 0) {
+ snapshot = getDiffSnapshot(index);
+ }
+
+ mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize());
+ mMemoryAllocatedText.pack();
+
+ mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize());
+ mDetailsTreeViewer.setInput(snapshot);
+ mDetailsTreeViewer.refresh();
+ }
+
+ /** Obtain the diff of snapshot[index] & snapshot[index-1] */
+ private NativeHeapSnapshot getDiffSnapshot(int index) {
+ // if it was already computed, simply return that
+ NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index);
+ if (diffSnapshot != null) {
+ return diffSnapshot;
+ }
+
+ // compute the diff
+ NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index);
+ NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1);
+ diffSnapshot = new NativeHeapDiffSnapshot(cur, prev);
+
+ // cache for future use
+ mDiffSnapshots.set(index, diffSnapshot);
+
+ return diffSnapshot;
+ }
+
+ private void updateDisplayGrouping() {
+ boolean groupByLibrary = mGroupByButton.getSelection();
+ mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary);
+
+ if (groupByLibrary) {
+ mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary);
+ } else {
+ mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations);
+ }
+ }
+
+ private void updateDisplayForZygotes() {
+ boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection();
+ mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory);
+
+ // inform the content providers of the zygote display setting
+ mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory);
+ mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory);
+
+ // refresh the UI
+ mDetailsTreeViewer.refresh();
+ }
+
+ private void updateSnapshotIndexCombo() {
+ List<String> items = new ArrayList<String>();
+
+ int numSnapshots = mNativeHeapSnapshots.size();
+ for (int i = 0; i < numSnapshots; i++) {
+ // offset indices by 1 so that users see index starting at 1 rather than 0
+ items.add("Snapshot " + (i + 1));
+ }
+
+ mSnapshotIndexCombo.setItems(items.toArray(new String[0]));
+
+ if (numSnapshots > 0) {
+ mSnapshotIndexCombo.setEnabled(true);
+ mSnapshotIndexCombo.select(numSnapshots - 1);
+ } else {
+ mSnapshotIndexCombo.setEnabled(false);
+ }
+ }
+
+ private void updateToolbars() {
+ int numSnapshots = mNativeHeapSnapshots.size();
+ mExportHeapDataButton.setEnabled(numSnapshots > 0);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(1, false));
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createControlsSection(c);
+ createDetailsSection(c);
+
+ // Initialize widget state based on whether a client
+ // is selected or not.
+ clientSelected();
+
+ return c;
+ }
+
+ private void createControlsSection(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(3, false));
+ c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ createGetHeapDataSection(c);
+
+ Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL);
+ l.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+ createDisplaySection(c);
+ }
+
+ private void createGetHeapDataSection(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(1, false));
+
+ createTakeHeapSnapshotButton(c);
+
+ Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ createLoadHeapDataButton(c);
+ }
+
+ private void createTakeHeapSnapshotButton(Composite parent) {
+ mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH);
+ mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT);
+ mSnapshotHeapButton.setLayoutData(new GridData());
+
+ // disable by default, enabled only when a client is selected
+ mSnapshotHeapButton.setEnabled(false);
+
+ mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ snapshotHeap();
+ }
+ });
+ }
+
+ private void snapshotHeap() {
+ Client c = getCurrentClient();
+ assert c != null : "Snapshot Heap could not have been enabled w/o a selected client.";
+
+ // send an async request
+ c.requestNativeHeapInformation();
+ }
+
+ private void createLoadHeapDataButton(Composite parent) {
+ mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH);
+ mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT);
+ mLoadHeapDataButton.setLayoutData(new GridData());
+
+ // disable by default, enabled only when a client is selected
+ mLoadHeapDataButton.setEnabled(false);
+
+ mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ loadHeapDataFromFile();
+ }
+ });
+ }
+
+ private void loadHeapDataFromFile() {
+ // pop up a file dialog and get the file to load
+ final String path = getHeapDumpToImport();
+ if (path == null) {
+ return;
+ }
+
+ Reader reader = null;
+ try {
+ reader = new FileReader(path);
+ } catch (FileNotFoundException e) {
+ // cannot occur since user input was via a FileDialog
+ }
+
+ Shell shell = Display.getDefault().getActiveShell();
+ ProgressMonitorDialog d = new ProgressMonitorDialog(shell);
+
+ NativeHeapDataImporter importer = new NativeHeapDataImporter(reader);
+ try {
+ d.run(true, true, importer);
+ } catch (InvocationTargetException e) {
+ // exception while parsing, display error to user and then return
+ MessageDialog.openError(shell,
+ "Error Importing Heap Data",
+ e.getCause().getMessage());
+ return;
+ } catch (InterruptedException e) {
+ // operation cancelled by user, simply return
+ return;
+ }
+
+ NativeHeapSnapshot snapshot = importer.getImportedSnapshot();
+
+ addToImportedSnapshots(snapshot); // save imported snapshot for future use
+ addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well
+
+ updateDisplay();
+ }
+
+ private void addToImportedSnapshots(NativeHeapSnapshot snapshot) {
+ Client c = getCurrentClient();
+
+ if (c == null) {
+ return;
+ }
+
+ Integer pid = c.getClientData().getPid();
+ List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(pid);
+ if (importedSnapshots == null) {
+ importedSnapshots = new ArrayList<NativeHeapSnapshot>();
+ }
+
+ importedSnapshots.add(snapshot);
+ mImportedSnapshotsPerPid.put(pid, importedSnapshots);
+ }
+
+ private String getHeapDumpToImport() {
+ FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(),
+ SWT.OPEN);
+
+ fileDialog.setText("Import Heap Dump");
+ fileDialog.setFilterExtensions(new String[] {"*.txt"});
+ fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH));
+
+ String selectedFile = fileDialog.open();
+ if (selectedFile != null) {
+ // save the path to restore in future dialog open
+ mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent());
+ }
+ return selectedFile;
+ }
+
+ private void createDisplaySection(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(2, false));
+ c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // Create: Display: __________________
+ createLabel(c, "Display:");
+ mSnapshotIndexCombo = new Combo(c, SWT.NONE | SWT.READ_ONLY);
+ mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."});
+ mSnapshotIndexCombo.setEnabled(false);
+ mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ displaySelectedSnapshot();
+ }
+ });
+
+ // Create: Memory Allocated (bytes): _________________
+ createLabel(c, "Memory Allocated:");
+ mMemoryAllocatedText = new Label(c, SWT.NONE);
+ GridData gd = new GridData();
+ gd.widthHint = 100;
+ mMemoryAllocatedText.setLayoutData(gd);
+
+ // Create: Search Path: __________________
+ createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT);
+ mSymbolSearchPathText = new Text(c, SWT.BORDER);
+ mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE);
+ mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT);
+ mSymbolSearchPathText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ String path = mSymbolSearchPathText.getText();
+ updateSearchPath(path);
+ mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path);
+ }
+ });
+ mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH));
+ mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ }
+
+ private void updateSearchPath(String path) {
+ Addr2Line.setSearchPath(path);
+ }
+
+ private void createLabel(Composite parent, String text) {
+ Label l = new Label(parent, SWT.NONE);
+ l.setText(text);
+ GridData gd = new GridData();
+ gd.horizontalAlignment = SWT.RIGHT;
+ l.setLayoutData(gd);
+ }
+
+ /**
+ * Create the details section displaying the details table and the stack trace
+ * corresponding to the selection.
+ *
+ * The details is laid out like so:
+ * Details Toolbar
+ * Details Table
+ * ------------sash---
+ * Stack Trace Label
+ * Stack Trace Text
+ * There is a sash in between the two sections, and we need to save/restore the sash
+ * preferences. Using FormLayout seems like the easiest solution here, but the layout
+ * code looks ugly as a result.
+ */
+ private void createDetailsSection(Composite parent) {
+ final Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new FormLayout());
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mDetailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER);
+ initializeDetailsToolBar(mDetailsToolBar);
+
+ Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI);
+ initializeDetailsTree(detailsTree);
+
+ final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER);
+
+ Label stackTraceLabel = new Label(c, SWT.NONE);
+ stackTraceLabel.setText("Stack Trace:");
+
+ Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI);
+ initializeStackTraceTree(stackTraceTree);
+
+ // layout the widgets created above
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mDetailsToolBar.setLayoutData(data);
+
+ data = new FormData();
+ data.top = new FormAttachment(mDetailsToolBar, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ detailsTree.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ sashData.top = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0);
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ stackTraceLabel.setLayoutData(data);
+
+ data = new FormData();
+ data.top = new FormAttachment(stackTraceLabel, 0);
+ data.left = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.right = new FormAttachment(100, 0);
+ stackTraceTree.setLayoutData(data);
+
+ sash.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = c.getClientArea();
+ int sashPercent = sashRect.y * 100 / panelRect.height;
+ mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent);
+
+ sashData.top = new FormAttachment(0, e.y);
+ c.layout();
+ }
+ });
+ }
+
+ private void initializeDetailsToolBar(ToolBar toolbar) {
+ mGroupByButton = new ToolItem(toolbar, SWT.CHECK);
+ mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE,
+ toolbar.getDisplay()));
+ mGroupByButton.setToolTipText(TOOLTIP_GROUPBY);
+ mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY));
+ mGroupByButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ updateDisplayGrouping();
+ }
+ });
+
+ mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK);
+ mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE,
+ toolbar.getDisplay()));
+ mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY);
+ mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY));
+ mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ // simply refresh the display, as the display logic takes care of
+ // the current state of the diffs only checkbox.
+ int idx = mSnapshotIndexCombo.getSelectionIndex();
+ displaySnapshot(idx);
+ }
+ });
+
+ mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK);
+ mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ ZYGOTE_IMAGE, toolbar.getDisplay()));
+ mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS);
+ mShowZygoteAllocationsButton.setSelection(
+ mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS));
+ mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ updateDisplayForZygotes();
+ }
+ });
+
+ mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH);
+ mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ EXPORT_DATA_IMAGE, toolbar.getDisplay()));
+ mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA);
+ mExportHeapDataButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ exportSnapshot();
+ }
+ });
+ }
+
+ /** Export currently displayed snapshot to a file */
+ private void exportSnapshot() {
+ int idx = mSnapshotIndexCombo.getSelectionIndex();
+ String snapshotName = mSnapshotIndexCombo.getItem(idx);
+
+ FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(),
+ SWT.SAVE);
+
+ fileDialog.setText("Save " + snapshotName);
+ fileDialog.setFileName("allocations.txt");
+
+ final String fileName = fileDialog.open();
+ if (fileName == null) {
+ return;
+ }
+
+ final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx);
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ PrintWriter out;
+ try {
+ out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
+ } catch (IOException e) {
+ displayErrorMessage(e.getMessage());
+ return;
+ }
+
+ for (NativeAllocationInfo alloc : snapshot.getAllocations()) {
+ out.println(alloc.toString());
+ }
+ out.close();
+ }
+
+ private void displayErrorMessage(final String message) {
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ MessageDialog.openError(Display.getDefault().getActiveShell(),
+ "Failed to export heap data", message);
+ }
+ });
+ }
+ });
+ t.setName("Saving Heap Data to File...");
+ t.start();
+ }
+
+ private void initializeDetailsTree(Tree tree) {
+ tree.setHeaderVisible(true);
+ tree.setLinesVisible(true);
+
+ List<String> properties = Arrays.asList(new String[] {
+ "Library",
+ "Total",
+ "Percentage",
+ "Count",
+ "Size",
+ "Method",
+ });
+
+ List<String> sampleValues = Arrays.asList(new String[] {
+ "/path/in/device/to/system/library.so",
+ "123456789",
+ " 100%",
+ "123456789",
+ "123456789",
+ "PossiblyLongDemangledMethodName",
+ });
+
+ // right align numeric values
+ List<Integer> swtFlags = Arrays.asList(new Integer[] {
+ SWT.LEFT,
+ SWT.RIGHT,
+ SWT.RIGHT,
+ SWT.RIGHT,
+ SWT.RIGHT,
+ SWT.LEFT,
+ });
+
+ for (int i = 0; i < properties.size(); i++) {
+ String p = properties.get(i);
+ String v = sampleValues.get(i);
+ int flags = swtFlags.get(i);
+ TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore);
+ }
+
+ mDetailsTreeViewer = new TreeViewer(tree);
+
+ mDetailsTreeViewer.setUseHashlookup(true);
+
+ boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS);
+ mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer,
+ displayZygotes);
+ mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer,
+ displayZygotes);
+ if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) {
+ mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary);
+ } else {
+ mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations);
+ }
+
+ mDetailsTreeLabelProvider = new NativeHeapLabelProvider();
+ mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider);
+
+ mDetailsTreeViewer.setInput(null);
+
+ tree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ displayStackTraceForSelection();
+ }
+ });
+ }
+
+ private void initializeStackTraceTree(Tree tree) {
+ tree.setHeaderVisible(true);
+ tree.setLinesVisible(true);
+
+ List<String> properties = Arrays.asList(new String[] {
+ "Address",
+ "Library",
+ "Method",
+ "File",
+ "Line",
+ });
+
+ List<String> sampleValues = Arrays.asList(new String[] {
+ "0x1234_5678",
+ "/path/in/device/to/system/library.so",
+ "PossiblyLongDemangledMethodName",
+ "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so",
+ "2000",
+ });
+
+ for (int i = 0; i < properties.size(); i++) {
+ String p = properties.get(i);
+ String v = sampleValues.get(i);
+ TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore);
+ }
+
+ mStackTraceTreeViewer = new TreeViewer(tree);
+
+ mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider());
+ mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider());
+
+ mStackTraceTreeViewer.setInput(null);
+ }
+
+ private void displayStackTraceForSelection() {
+ TreeItem []items = mDetailsTreeViewer.getTree().getSelection();
+ if (items.length == 0) {
+ mStackTraceTreeViewer.setInput(null);
+ return;
+ }
+
+ Object data = items[0].getData();
+ if (!(data instanceof NativeAllocationInfo)) {
+ mStackTraceTreeViewer.setInput(null);
+ return;
+ }
+
+ NativeAllocationInfo info = (NativeAllocationInfo) data;
+ if (info.isStackCallResolved()) {
+ mStackTraceTreeViewer.setInput(info.getResolvedStackCall());
+ } else {
+ mStackTraceTreeViewer.setInput(info.getStackCallAddresses());
+ }
+ }
+
+ private String getPref(String prefix, String s) {
+ return "nativeheap.tree." + prefix + "." + s;
+ }
+
+ @Override
+ public void setFocus() {
+ }
+
+ private ITableFocusListener mTableFocusListener;
+
+ @Override
+ public void setTableFocusListener(ITableFocusListener listener) {
+ mTableFocusListener = listener;
+
+ final Tree heapSitesTree = mDetailsTreeViewer.getTree();
+ final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() {
+ @Override
+ public void copy(Clipboard clipboard) {
+ TreeItem[] items = heapSitesTree.getSelection();
+ copyToClipboard(items, clipboard);
+ }
+
+ @Override
+ public void selectAll() {
+ heapSitesTree.selectAll();
+ }
+ };
+
+ heapSitesTree.addFocusListener(new FocusListener() {
+ @Override
+ public void focusLost(FocusEvent arg0) {
+ mTableFocusListener.focusLost(heapSitesActivator);
+ }
+
+ @Override
+ public void focusGained(FocusEvent arg0) {
+ mTableFocusListener.focusGained(heapSitesActivator);
+ }
+ });
+
+ final Tree stackTraceTree = mStackTraceTreeViewer.getTree();
+ final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() {
+ @Override
+ public void copy(Clipboard clipboard) {
+ TreeItem[] items = stackTraceTree.getSelection();
+ copyToClipboard(items, clipboard);
+ }
+
+ @Override
+ public void selectAll() {
+ stackTraceTree.selectAll();
+ }
+ };
+
+ stackTraceTree.addFocusListener(new FocusListener() {
+ @Override
+ public void focusLost(FocusEvent arg0) {
+ mTableFocusListener.focusLost(stackTraceActivator);
+ }
+
+ @Override
+ public void focusGained(FocusEvent arg0) {
+ mTableFocusListener.focusGained(stackTraceActivator);
+ }
+ });
+ }
+
+ private void copyToClipboard(TreeItem[] items, Clipboard clipboard) {
+ StringBuilder sb = new StringBuilder();
+
+ for (TreeItem item : items) {
+ Object data = item.getData();
+ if (data != null) {
+ sb.append(data.toString());
+ sb.append('\n');
+ }
+ }
+
+ String content = sb.toString();
+ if (content.length() > 0) {
+ clipboard.setContents(
+ new Object[] {sb.toString()},
+ new Transfer[] {TextTransfer.getInstance()}
+ );
+ }
+ }
+
+ private class SymbolResolverTask implements Runnable {
+ private List<NativeAllocationInfo> mCallSites;
+ private List<NativeLibraryMapInfo> mMappedLibraries;
+ private Map<Long, NativeStackCallInfo> mResolvedSymbolCache;
+
+ public SymbolResolverTask(List<NativeAllocationInfo> callSites,
+ List<NativeLibraryMapInfo> mappedLibraries) {
+ mCallSites = callSites;
+ mMappedLibraries = mappedLibraries;
+
+ mResolvedSymbolCache = new HashMap<Long, NativeStackCallInfo>();
+ }
+
+ @Override
+ public void run() {
+ for (NativeAllocationInfo callSite : mCallSites) {
+ if (callSite.isStackCallResolved()) {
+ continue;
+ }
+
+ List<Long> addresses = callSite.getStackCallAddresses();
+ List<NativeStackCallInfo> resolvedStackInfo =
+ new ArrayList<NativeStackCallInfo>(addresses.size());
+
+ for (Long address : addresses) {
+ NativeStackCallInfo info = mResolvedSymbolCache.get(address);
+
+ if (info != null) {
+ resolvedStackInfo.add(info);
+ } else {
+ info = resolveAddress(address);
+ resolvedStackInfo.add(info);
+ mResolvedSymbolCache.put(address, info);
+ }
+ }
+
+ callSite.setResolvedStackCall(resolvedStackInfo);
+ }
+
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mDetailsTreeViewer.refresh();
+ mStackTraceTreeViewer.refresh();
+ }
+ });
+ }
+
+ private NativeStackCallInfo resolveAddress(long addr) {
+ NativeLibraryMapInfo library = getLibraryFor(addr);
+
+ if (library != null) {
+ Addr2Line process = Addr2Line.getProcess(library);
+ if (process != null) {
+ NativeStackCallInfo info = process.getAddress(addr);
+ if (info != null) {
+ return info;
+ }
+ }
+ }
+
+ return new NativeStackCallInfo(addr,
+ library != null ? library.getLibraryName() : null,
+ Long.toHexString(addr),
+ "");
+ }
+
+ private NativeLibraryMapInfo getLibraryFor(long addr) {
+ for (NativeLibraryMapInfo info : mMappedLibraries) {
+ if (info.isWithinLibrary(addr)) {
+ return info;
+ }
+ }
+
+ Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
+ return null;
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java
new file mode 100644
index 0000000..c31716b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import org.eclipse.jface.viewers.ILazyTreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}.
+ * It expects a {@link NativeHeapSnapshot} as input, and provides the list of allocations
+ * in the heap dump as content to the UI.
+ */
+public final class NativeHeapProviderByAllocations implements ILazyTreeContentProvider {
+ private TreeViewer mViewer;
+ private boolean mDisplayZygoteMemory;
+ private NativeHeapSnapshot mNativeHeapDump;
+
+ public NativeHeapProviderByAllocations(TreeViewer viewer, boolean displayZygotes) {
+ mViewer = viewer;
+ mDisplayZygoteMemory = displayZygotes;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ mNativeHeapDump = (NativeHeapSnapshot) newInput;
+ }
+
+ @Override
+ public Object getParent(Object arg0) {
+ return null;
+ }
+
+ @Override
+ public void updateChildCount(Object element, int currentChildCount) {
+ int childCount = 0;
+
+ if (element == mNativeHeapDump) { // root element
+ childCount = getAllocations().size();
+ }
+
+ mViewer.setChildCount(element, childCount);
+ }
+
+ @Override
+ public void updateElement(Object parent, int index) {
+ Object item = null;
+
+ if (parent == mNativeHeapDump) { // root element
+ item = getAllocations().get(index);
+ }
+
+ mViewer.replace(parent, index, item);
+ mViewer.setChildCount(item, 0);
+ }
+
+ public void displayZygoteMemory(boolean en) {
+ mDisplayZygoteMemory = en;
+ }
+
+ private List<NativeAllocationInfo> getAllocations() {
+ if (mDisplayZygoteMemory) {
+ return mNativeHeapDump.getAllocations();
+ } else {
+ return mNativeHeapDump.getNonZygoteAllocations();
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java
new file mode 100644
index 0000000..b786bfa
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import org.eclipse.jface.viewers.ILazyTreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}.
+ * It expects input of type {@link NativeHeapSnapshot}, and provides heap allocations
+ * grouped by library to the UI.
+ */
+public class NativeHeapProviderByLibrary implements ILazyTreeContentProvider {
+ private TreeViewer mViewer;
+ private boolean mDisplayZygoteMemory;
+
+ public NativeHeapProviderByLibrary(TreeViewer viewer, boolean displayZygotes) {
+ mViewer = viewer;
+ mDisplayZygoteMemory = displayZygotes;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ return null;
+ }
+
+ @Override
+ public void updateChildCount(Object element, int currentChildCount) {
+ int childCount = 0;
+
+ if (element instanceof NativeHeapSnapshot) {
+ NativeHeapSnapshot snapshot = (NativeHeapSnapshot) element;
+ childCount = getLibraryAllocations(snapshot).size();
+ }
+
+ mViewer.setChildCount(element, childCount);
+ }
+
+ @Override
+ public void updateElement(Object parent, int index) {
+ Object item = null;
+ int childCount = 0;
+
+ if (parent instanceof NativeHeapSnapshot) { // root element
+ NativeHeapSnapshot snapshot = (NativeHeapSnapshot) parent;
+ item = getLibraryAllocations(snapshot).get(index);
+ childCount = ((NativeLibraryAllocationInfo) item).getAllocations().size();
+ } else if (parent instanceof NativeLibraryAllocationInfo) {
+ item = ((NativeLibraryAllocationInfo) parent).getAllocations().get(index);
+ }
+
+ mViewer.replace(parent, index, item);
+ mViewer.setChildCount(item, childCount);
+ }
+
+ public void displayZygoteMemory(boolean en) {
+ mDisplayZygoteMemory = en;
+ }
+
+ private List<NativeLibraryAllocationInfo> getLibraryAllocations(NativeHeapSnapshot snapshot) {
+ if (mDisplayZygoteMemory) {
+ return snapshot.getAllocationsByLibrary();
+ } else {
+ return snapshot.getNonZygoteAllocationsByLibrary();
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java
new file mode 100644
index 0000000..e2023d2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A Native Heap Snapshot models a single heap dump.
+ *
+ * It primarily consists of a list of {@link NativeAllocationInfo} objects. From this list,
+ * other objects of interest to the UI are computed and cached for future use.
+ */
+public class NativeHeapSnapshot {
+ private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance();
+
+ private List<NativeAllocationInfo> mHeapAllocations;
+ private List<NativeLibraryAllocationInfo> mHeapAllocationsByLibrary;
+
+ private List<NativeAllocationInfo> mNonZygoteHeapAllocations;
+ private List<NativeLibraryAllocationInfo> mNonZygoteHeapAllocationsByLibrary;
+
+ private long mTotalSize;
+
+ public NativeHeapSnapshot(List<NativeAllocationInfo> heapAllocations) {
+ mHeapAllocations = heapAllocations;
+
+ // precompute the total size as this is always needed.
+ mTotalSize = getTotalMemory(heapAllocations);
+ }
+
+ protected long getTotalMemory(Collection<NativeAllocationInfo> heapSnapshot) {
+ long total = 0;
+
+ for (NativeAllocationInfo info : heapSnapshot) {
+ total += info.getAllocationCount() * info.getSize();
+ }
+
+ return total;
+ }
+
+ public List<NativeAllocationInfo> getAllocations() {
+ return mHeapAllocations;
+ }
+
+ public List<NativeLibraryAllocationInfo> getAllocationsByLibrary() {
+ if (mHeapAllocationsByLibrary != null) {
+ return mHeapAllocationsByLibrary;
+ }
+
+ List<NativeLibraryAllocationInfo> heapAllocations =
+ NativeLibraryAllocationInfo.constructFrom(mHeapAllocations);
+
+ // cache for future uses only if it is fully resolved.
+ if (isFullyResolved(heapAllocations)) {
+ mHeapAllocationsByLibrary = heapAllocations;
+ }
+
+ return heapAllocations;
+ }
+
+ private boolean isFullyResolved(List<NativeLibraryAllocationInfo> heapAllocations) {
+ for (NativeLibraryAllocationInfo info : heapAllocations) {
+ if (info.getLibraryName().equals(NativeLibraryAllocationInfo.UNRESOLVED_LIBRARY_NAME)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public long getTotalSize() {
+ return mTotalSize;
+ }
+
+ public String getFormattedMemorySize() {
+ return String.format("%s bytes", formatMemorySize(getTotalSize()));
+ }
+
+ protected String formatMemorySize(long memSize) {
+ return NUMBER_FORMATTER.format(memSize);
+ }
+
+ public List<NativeAllocationInfo> getNonZygoteAllocations() {
+ if (mNonZygoteHeapAllocations != null) {
+ return mNonZygoteHeapAllocations;
+ }
+
+ // filter out all zygote allocations
+ mNonZygoteHeapAllocations = new ArrayList<NativeAllocationInfo>();
+ for (NativeAllocationInfo info : mHeapAllocations) {
+ if (info.isZygoteChild()) {
+ mNonZygoteHeapAllocations.add(info);
+ }
+ }
+
+ return mNonZygoteHeapAllocations;
+ }
+
+ public List<NativeLibraryAllocationInfo> getNonZygoteAllocationsByLibrary() {
+ if (mNonZygoteHeapAllocationsByLibrary != null) {
+ return mNonZygoteHeapAllocationsByLibrary;
+ }
+
+ List<NativeLibraryAllocationInfo> heapAllocations =
+ NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations());
+
+ // cache for future uses only if it is fully resolved.
+ if (isFullyResolved(heapAllocations)) {
+ mNonZygoteHeapAllocationsByLibrary = heapAllocations;
+ }
+
+ return heapAllocations;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java
new file mode 100644
index 0000000..1722cdb
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A heap dump representation where each call site is associated with its source library.
+ */
+public final class NativeLibraryAllocationInfo {
+ /** Library name to use when grouping before symbol resolution is complete. */
+ public static final String UNRESOLVED_LIBRARY_NAME = "Resolving..";
+
+ /** Any call site that cannot be resolved to a specific library goes under this name. */
+ private static final String UNKNOWN_LIBRARY_NAME = "unknown";
+
+ private final String mLibraryName;
+ private final List<NativeAllocationInfo> mHeapAllocations;
+ private int mTotalSize;
+
+ private NativeLibraryAllocationInfo(String libraryName) {
+ mLibraryName = libraryName;
+ mHeapAllocations = new ArrayList<NativeAllocationInfo>();
+ }
+
+ private void addAllocation(NativeAllocationInfo info) {
+ mHeapAllocations.add(info);
+ }
+
+ private void updateTotalSize() {
+ mTotalSize = 0;
+ for (NativeAllocationInfo i : mHeapAllocations) {
+ mTotalSize += i.getAllocationCount() * i.getSize();
+ }
+ }
+
+ public String getLibraryName() {
+ return mLibraryName;
+ }
+
+ public long getTotalSize() {
+ return mTotalSize;
+ }
+
+ public List<NativeAllocationInfo> getAllocations() {
+ return mHeapAllocations;
+ }
+
+ /**
+ * Factory method to create a list of {@link NativeLibraryAllocationInfo} objects,
+ * given the list of {@link NativeAllocationInfo} objects.
+ *
+ * If the {@link NativeAllocationInfo} objects do not have their symbols resolved,
+ * then they are grouped under the library {@link #UNRESOLVED_LIBRARY_NAME}. If they do
+ * have their symbols resolved, but map to an unknown library, then they are grouped under
+ * the library {@link #UNKNOWN_LIBRARY_NAME}.
+ */
+ public static List<NativeLibraryAllocationInfo> constructFrom(
+ List<NativeAllocationInfo> allocations) {
+ if (allocations == null) {
+ return null;
+ }
+
+ Map<String, NativeLibraryAllocationInfo> allocationsByLibrary =
+ new HashMap<String, NativeLibraryAllocationInfo>();
+
+ // go through each native allocation and assign it to the appropriate library
+ for (NativeAllocationInfo info : allocations) {
+ String libName = UNRESOLVED_LIBRARY_NAME;
+
+ if (info.isStackCallResolved()) {
+ NativeStackCallInfo relevantStackCall = info.getRelevantStackCallInfo();
+ if (relevantStackCall != null) {
+ libName = relevantStackCall.getLibraryName();
+ } else {
+ libName = UNKNOWN_LIBRARY_NAME;
+ }
+ }
+
+ addtoLibrary(allocationsByLibrary, libName, info);
+ }
+
+ List<NativeLibraryAllocationInfo> libraryAllocations =
+ new ArrayList<NativeLibraryAllocationInfo>(allocationsByLibrary.values());
+
+ // now update some summary statistics for each library
+ for (NativeLibraryAllocationInfo l : libraryAllocations) {
+ l.updateTotalSize();
+ }
+
+ // finally, sort by total size
+ Collections.sort(libraryAllocations, new Comparator<NativeLibraryAllocationInfo>() {
+ @Override
+ public int compare(NativeLibraryAllocationInfo o1,
+ NativeLibraryAllocationInfo o2) {
+ return (int) (o2.getTotalSize() - o1.getTotalSize());
+ }
+ });
+
+ return libraryAllocations;
+ }
+
+ private static void addtoLibrary(Map<String, NativeLibraryAllocationInfo> libraryAllocations,
+ String libName, NativeAllocationInfo info) {
+ NativeLibraryAllocationInfo libAllocationInfo = libraryAllocations.get(libName);
+ if (libAllocationInfo == null) {
+ libAllocationInfo = new NativeLibraryAllocationInfo(libName);
+ libraryAllocations.put(libName, libAllocationInfo);
+ }
+
+ libAllocationInfo.addAllocation(info);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java
new file mode 100644
index 0000000..9a6ddb2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+public class NativeStackContentProvider implements ITreeContentProvider {
+ @Override
+ public Object[] getElements(Object arg0) {
+ return getChildren(arg0);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof List<?>) {
+ return ((List<?>) parentElement).toArray();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ return false;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java
new file mode 100644
index 0000000..b7428b9
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+public class NativeStackLabelProvider extends LabelProvider implements ITableLabelProvider {
+ @Override
+ public Image getColumnImage(Object arg0, int arg1) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int index) {
+ if (element instanceof NativeStackCallInfo) {
+ return getResolvedStackTraceColumnText((NativeStackCallInfo) element, index);
+ }
+
+ if (element instanceof Long) {
+ // if the addresses have not been resolved, then just display the
+ // addresses alone
+ return getStackAddressColumnText((Long) element, index);
+ }
+
+ return null;
+ }
+
+ public String getResolvedStackTraceColumnText(NativeStackCallInfo info, int index) {
+ switch (index) {
+ case 0:
+ return String.format("0x%08x", info.getAddress());
+ case 1:
+ return info.getLibraryName();
+ case 2:
+ return info.getMethodName();
+ case 3:
+ return info.getSourceFile();
+ case 4:
+ int l = info.getLineNumber();
+ return l == -1 ? "" : Integer.toString(l);
+ }
+
+ return null;
+ }
+
+ private String getStackAddressColumnText(Long address, int index) {
+ if (index == 0) {
+ return String.format("0x%08x", address);
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java
new file mode 100644
index 0000000..1a75c6e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.DdmUiPreferences;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A symbol resolver task that can resolve a set of addresses to their corresponding
+ * source method name + file name:line number.
+ *
+ * It first identifies the library that contains the address, and then runs addr2line on
+ * the library to get the symbol name + source location.
+ */
+public class NativeSymbolResolverTask implements IRunnableWithProgress {
+ private static final String ADDR2LINE;
+ private static final String DEFAULT_SYMBOLS_FOLDER;
+
+ static {
+ String addr2lineEnv = System.getenv("ANDROID_ADDR2LINE");
+ ADDR2LINE = addr2lineEnv != null ? addr2lineEnv : DdmUiPreferences.getAddr2Line();
+
+ String symbols = System.getenv("ANDROID_SYMBOLS");
+ DEFAULT_SYMBOLS_FOLDER = symbols != null ? symbols : DdmUiPreferences.getSymbolDirectory();
+ }
+
+ private List<NativeAllocationInfo> mCallSites;
+ private List<NativeLibraryMapInfo> mMappedLibraries;
+ private List<String> mSymbolSearchFolders;
+
+ /** All unresolved addresses from all the callsites. */
+ private SortedSet<Long> mUnresolvedAddresses;
+
+ /** Set of all addresses that could were not resolved at the end of the resolution process. */
+ private Set<Long> mUnresolvableAddresses;
+
+ /** Map of library -> [unresolved addresses mapping to this library]. */
+ private Map<NativeLibraryMapInfo, Set<Long>> mUnresolvedAddressesPerLibrary;
+
+ /** Addresses that could not be mapped to a library, should be mostly empty. */
+ private Set<Long> mUnmappedAddresses;
+
+ /** Cache of the resolution for every unresolved address. */
+ private Map<Long, NativeStackCallInfo> mAddressResolution;
+
+ /** List of libraries that were not located on disk. */
+ private Set<String> mNotFoundLibraries;
+ private String mAddr2LineErrorMessage = null;
+
+ public NativeSymbolResolverTask(List<NativeAllocationInfo> callSites,
+ List<NativeLibraryMapInfo> mappedLibraries,
+ String symbolSearchPath) {
+ mCallSites = callSites;
+ mMappedLibraries = mappedLibraries;
+ mSymbolSearchFolders = new ArrayList<String>();
+ mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER);
+ mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":")));
+
+ mUnresolvedAddresses = new TreeSet<Long>();
+ mUnresolvableAddresses = new HashSet<Long>();
+ mUnresolvedAddressesPerLibrary = new HashMap<NativeLibraryMapInfo, Set<Long>>();
+ mUnmappedAddresses = new HashSet<Long>();
+ mAddressResolution = new HashMap<Long, NativeStackCallInfo>();
+ mNotFoundLibraries = new HashSet<String>();
+ }
+
+ @Override
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException, InterruptedException {
+ monitor.beginTask("Resolving symbols", IProgressMonitor.UNKNOWN);
+
+ collectAllUnresolvedAddresses();
+ checkCancellation(monitor);
+
+ mapUnresolvedAddressesToLibrary();
+ checkCancellation(monitor);
+
+ resolveLibraryAddresses(monitor);
+ checkCancellation(monitor);
+
+ resolveCallSites(mCallSites);
+
+ monitor.done();
+ }
+
+ private void collectAllUnresolvedAddresses() {
+ for (NativeAllocationInfo callSite : mCallSites) {
+ mUnresolvedAddresses.addAll(callSite.getStackCallAddresses());
+ }
+ }
+
+ private void mapUnresolvedAddressesToLibrary() {
+ Set<Long> mappedAddresses = new HashSet<Long>();
+
+ for (NativeLibraryMapInfo lib : mMappedLibraries) {
+ SortedSet<Long> addressesInLibrary = mUnresolvedAddresses.subSet(lib.getStartAddress(),
+ lib.getEndAddress() + 1);
+ if (addressesInLibrary.size() > 0) {
+ mUnresolvedAddressesPerLibrary.put(lib, addressesInLibrary);
+ mappedAddresses.addAll(addressesInLibrary);
+ }
+ }
+
+ // unmapped addresses = unresolved addresses - mapped addresses
+ mUnmappedAddresses.addAll(mUnresolvedAddresses);
+ mUnmappedAddresses.removeAll(mappedAddresses);
+ }
+
+ private void resolveLibraryAddresses(IProgressMonitor monitor) throws InterruptedException {
+ for (NativeLibraryMapInfo lib : mUnresolvedAddressesPerLibrary.keySet()) {
+ String libPath = getLibraryLocation(lib);
+ Set<Long> addressesToResolve = mUnresolvedAddressesPerLibrary.get(lib);
+
+ if (libPath == null) {
+ mNotFoundLibraries.add(lib.getLibraryName());
+ markAddressesNotResolvable(addressesToResolve, lib);
+ } else {
+ monitor.subTask(String.format("Resolving addresses mapped to %s.", libPath));
+ resolveAddresses(lib, libPath, addressesToResolve);
+ }
+
+ checkCancellation(monitor);
+ }
+ }
+
+ private void resolveAddresses(NativeLibraryMapInfo lib, String libPath,
+ Set<Long> addressesToResolve) {
+ Process addr2line = null;
+ try {
+ addr2line = new ProcessBuilder(ADDR2LINE,
+ "-C", // demangle
+ "-f", // display function names in addition to file:number
+ "-e", libPath).start();
+ } catch (IOException e) {
+ // Since the library path is known to be valid, the only reason for an exception
+ // is that addr2line was not found. We just save the message in this case.
+ mAddr2LineErrorMessage = e.getMessage();
+ markAddressesNotResolvable(addressesToResolve, lib);
+ return;
+ }
+
+ BufferedReader resultReader = new BufferedReader(new InputStreamReader(
+ addr2line.getInputStream()));
+ BufferedWriter addressWriter = new BufferedWriter(new OutputStreamWriter(
+ addr2line.getOutputStream()));
+
+ long libStartAddress = isExecutable(lib) ? 0 : lib.getStartAddress();
+ try {
+ for (Long addr : addressesToResolve) {
+ long offset = addr.longValue() - libStartAddress;
+ addressWriter.write(Long.toHexString(offset));
+ addressWriter.newLine();
+ addressWriter.flush();
+ String method = resultReader.readLine();
+ String sourceFile = resultReader.readLine();
+
+ mAddressResolution.put(addr,
+ new NativeStackCallInfo(addr.longValue(),
+ lib.getLibraryName(),
+ method,
+ sourceFile));
+ }
+ } catch (IOException e) {
+ // if there is any error, then mark the addresses not already resolved
+ // as unresolvable.
+ for (Long addr : addressesToResolve) {
+ if (mAddressResolution.get(addr) == null) {
+ markAddressNotResolvable(lib, addr);
+ }
+ }
+ }
+
+ try {
+ resultReader.close();
+ addressWriter.close();
+ } catch (IOException e) {
+ // we can ignore these exceptions
+ }
+
+ addr2line.destroy();
+ }
+
+ private boolean isExecutable(NativeLibraryMapInfo object) {
+ // TODO: Use a tool like readelf or nm to determine whether this object is a library
+ // or an executable.
+ // For now, we'll just assume that any object present in the bin folder is an executable.
+ String devicePath = object.getLibraryName();
+ return devicePath.contains("/bin/");
+ }
+
+ private void markAddressesNotResolvable(Set<Long> addressesToResolve,
+ NativeLibraryMapInfo lib) {
+ for (Long addr : addressesToResolve) {
+ markAddressNotResolvable(lib, addr);
+ }
+ }
+
+ private void markAddressNotResolvable(NativeLibraryMapInfo lib, Long addr) {
+ mAddressResolution.put(addr,
+ new NativeStackCallInfo(addr.longValue(),
+ lib.getLibraryName(),
+ Long.toHexString(addr),
+ ""));
+ mUnresolvableAddresses.add(addr);
+ }
+
+ /**
+ * Locate on local disk the debug library w/ symbols corresponding to the
+ * library on the device. It searches for this library in the symbol path.
+ * @return absolute path if found, null otherwise
+ */
+ private String getLibraryLocation(NativeLibraryMapInfo lib) {
+ String pathOnDevice = lib.getLibraryName();
+ String libName = new File(pathOnDevice).getName();
+
+ for (String p : mSymbolSearchFolders) {
+ // try appending the full path on device
+ String fullPath = p + File.separator + pathOnDevice;
+ if (new File(fullPath).exists()) {
+ return fullPath;
+ }
+
+ // try appending basename(library)
+ fullPath = p + File.separator + libName;
+ if (new File(fullPath).exists()) {
+ return fullPath;
+ }
+ }
+
+ return null;
+ }
+
+ private void resolveCallSites(List<NativeAllocationInfo> callSites) {
+ for (NativeAllocationInfo callSite : callSites) {
+ List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>();
+
+ for (Long addr : callSite.getStackCallAddresses()) {
+ NativeStackCallInfo info = mAddressResolution.get(addr);
+
+ if (info != null) {
+ stackInfo.add(info);
+ }
+ }
+
+ callSite.setResolvedStackCall(stackInfo);
+ }
+ }
+
+ private void checkCancellation(IProgressMonitor monitor) throws InterruptedException {
+ if (monitor.isCanceled()) {
+ throw new InterruptedException();
+ }
+ }
+
+ public String getAddr2LineErrorMessage() {
+ return mAddr2LineErrorMessage;
+ }
+
+ public Set<Long> getUnmappedAddresses() {
+ return mUnmappedAddresses;
+ }
+
+ public Set<Long> getUnresolvableAddresses() {
+ return mUnresolvableAddresses;
+ }
+
+ public Set<String> getNotFoundLibraries() {
+ return mNotFoundLibraries;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java
new file mode 100644
index 0000000..2aef53c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+
+/**
+ * Encapsulation of controls handling a location coordinate in decimal and sexagesimal.
+ * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener}
+ * on all the {@link Text} widgets.
+ * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by
+ * a call to {@link #isValueValid()})
+ */
+public final class CoordinateControls {
+ private double mValue;
+ private boolean mValueValidity = false;
+ private Text mDecimalText;
+ private Text mSexagesimalDegreeText;
+ private Text mSexagesimalMinuteText;
+ private Text mSexagesimalSecondText;
+ private final DecimalFormat mDecimalFormat = new DecimalFormat();
+
+ /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)}
+ * is called. This is an int instead of a boolean to act as a counter. */
+ private int mManualTextChange = 0;
+
+ /**
+ * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode.
+ */
+ private ModifyListener mSexagesimalListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = getValueFromSexagesimalControls();
+ setValueIntoDecimalControl(mValue);
+ mValueValidity = true;
+ } catch (ParseException e) {
+ // wrong format empty the decimal controls.
+ mValueValidity = false;
+ resetDecimalControls();
+ }
+ }
+ };
+
+ /**
+ * Creates the {@link Text} control for the decimal display of the coordinate.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createDecimalText(Composite parent) {
+ mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue();
+ setValueIntoSexagesimalControl(mValue);
+ mValueValidity = true;
+ } catch (ParseException e) {
+ // wrong format empty the sexagesimal controls.
+ mValueValidity = false;
+ resetSexagesimalControls();
+ }
+ }
+ });
+ }
+
+ /**
+ * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalDegreeText(Composite parent) {
+ mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalMinuteText(Composite parent) {
+ mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalSecondText(Composite parent) {
+ mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Sets the coordinate into the {@link Text} controls.
+ * @param value the coordinate value to set.
+ */
+ public void setValue(double value) {
+ mValue = value;
+ mValueValidity = true;
+ setValueIntoDecimalControl(value);
+ setValueIntoSexagesimalControl(value);
+ }
+
+ /**
+ * Returns whether the value in the control(s) is valid.
+ */
+ public boolean isValueValid() {
+ return mValueValidity;
+ }
+
+ /**
+ * Returns the current value set in the control(s).
+ * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed
+ * before any call to this method.
+ */
+ public double getValue() {
+ return mValue;
+ }
+
+ /**
+ * Enables or disables all the {@link Text} controls.
+ * @param enabled the enabled state.
+ */
+ public void setEnabled(boolean enabled) {
+ mDecimalText.setEnabled(enabled);
+ mSexagesimalDegreeText.setEnabled(enabled);
+ mSexagesimalMinuteText.setEnabled(enabled);
+ mSexagesimalSecondText.setEnabled(enabled);
+ }
+
+ private void resetDecimalControls() {
+ mManualTextChange++;
+ mDecimalText.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+
+ private void resetSexagesimalControls() {
+ mManualTextChange++;
+ mSexagesimalDegreeText.setText(""); //$NON-NLS-1$
+ mSexagesimalMinuteText.setText(""); //$NON-NLS-1$
+ mSexagesimalSecondText.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+
+ /**
+ * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener}
+ * @param parent the parent {@link Composite}.
+ * @param defaultString the default string to be used to compute the {@link Text} control
+ * size hint.
+ * @param listener the {@link ModifyListener} to be called when the {@link Text} control is
+ * modified.
+ */
+ private Text createTextControl(Composite parent, String defaultString,
+ ModifyListener listener) {
+ // create the control
+ Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+
+ // add the standard listener to it.
+ text.addModifyListener(listener);
+
+ // compute its size/
+ mManualTextChange++;
+ text.setText(defaultString);
+ text.pack();
+ Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ text.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+
+ GridData gridData = new GridData();
+ gridData.widthHint = size.x;
+ text.setLayoutData(gridData);
+
+ return text;
+ }
+
+ private double getValueFromSexagesimalControls() throws ParseException {
+ double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue();
+ double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue();
+ double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue();
+
+ boolean isPositive = (degrees >= 0.);
+ degrees = Math.abs(degrees);
+
+ double value = degrees + minutes / 60. + seconds / 3600.;
+ return isPositive ? value : - value;
+ }
+
+ private void setValueIntoDecimalControl(double value) {
+ mManualTextChange++;
+ mDecimalText.setText(String.format("%.6f", value));
+ mManualTextChange--;
+ }
+
+ private void setValueIntoSexagesimalControl(double value) {
+ // get the sign and make the number positive no matter what.
+ boolean isPositive = (value >= 0.);
+ value = Math.abs(value);
+
+ // get the degree
+ double degrees = Math.floor(value);
+
+ // get the minutes
+ double minutes = Math.floor((value - degrees) * 60.);
+
+ // get the seconds.
+ double seconds = (value - degrees) * 3600. - minutes * 60.;
+
+ mManualTextChange++;
+ mSexagesimalDegreeText.setText(
+ Integer.toString(isPositive ? (int)degrees : (int)- degrees));
+ mSexagesimalMinuteText.setText(Integer.toString((int)minutes));
+ mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java
new file mode 100644
index 0000000..a30337a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic GPX parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic waypoint information, and tracks (merging segments).
+ */
+public class GpxParser {
+
+ private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$
+
+ private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$
+ private final static String NODE_TRACK = "trk"; //$NON-NLS-1$
+ private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$
+ private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$
+ private final static String NODE_NAME = "name"; //$NON-NLS-1$
+ private final static String NODE_TIME = "time"; //$NON-NLS-1$
+ private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$
+ private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$
+ private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$
+ private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$
+
+ private static SAXParserFactory sParserFactory;
+
+ static {
+ sParserFactory = SAXParserFactory.newInstance();
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ private String mFileName;
+
+ private GpxHandler mHandler;
+
+ /** Pattern to parse time with optional sub-second precision, and optional
+ * Z indicating the time is in UTC. */
+ private final static Pattern ISO8601_TIME =
+ Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$
+
+ /**
+ * Handler for the SAX parser.
+ */
+ private static class GpxHandler extends DefaultHandler {
+ // --------- parsed data ---------
+ List<WayPoint> mWayPoints;
+ List<Track> mTrackList;
+
+ // --------- state for parsing ---------
+ Track mCurrentTrack;
+ TrackPoint mCurrentTrackPoint;
+ WayPoint mCurrentWayPoint;
+ final StringBuilder mStringAccumulator = new StringBuilder();
+
+ boolean mSuccess = true;
+
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ // we only care about the standard GPX nodes.
+ try {
+ if (NS_GPX.equals(uri)) {
+ if (NODE_WAYPOINT.equals(localName)) {
+ if (mWayPoints == null) {
+ mWayPoints = new ArrayList<WayPoint>();
+ }
+
+ mWayPoints.add(mCurrentWayPoint = new WayPoint());
+ handleLocation(mCurrentWayPoint, attributes);
+ } else if (NODE_TRACK.equals(localName)) {
+ if (mTrackList == null) {
+ mTrackList = new ArrayList<Track>();
+ }
+
+ mTrackList.add(mCurrentTrack = new Track());
+ } else if (NODE_TRACK_SEGMENT.equals(localName)) {
+ // for now we do nothing here. This will merge all the segments into
+ // a single TrackPoint list in the Track.
+ } else if (NODE_TRACK_POINT.equals(localName)) {
+ if (mCurrentTrack != null) {
+ mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint());
+ handleLocation(mCurrentTrackPoint, attributes);
+ }
+ }
+ }
+ } finally {
+ // no matter the node, we empty the StringBuilder accumulator when we start
+ // a new node.
+ mStringAccumulator.setLength(0);
+ }
+ }
+
+ /**
+ * Processes new characters for the node content. The characters are simply stored,
+ * and will be processed when {@link #endElement(String, String, String)} is called.
+ */
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mStringAccumulator.append(ch, start, length);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+ if (NS_GPX.equals(uri)) {
+ if (NODE_WAYPOINT.equals(localName)) {
+ mCurrentWayPoint = null;
+ } else if (NODE_TRACK.equals(localName)) {
+ mCurrentTrack = null;
+ } else if (NODE_TRACK_POINT.equals(localName)) {
+ mCurrentTrackPoint = null;
+ } else if (NODE_NAME.equals(localName)) {
+ if (mCurrentTrack != null) {
+ mCurrentTrack.setName(mStringAccumulator.toString());
+ } else if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setName(mStringAccumulator.toString());
+ }
+ } else if (NODE_TIME.equals(localName)) {
+ if (mCurrentTrackPoint != null) {
+ mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString()));
+ }
+ } else if (NODE_ELEVATION.equals(localName)) {
+ if (mCurrentTrackPoint != null) {
+ mCurrentTrackPoint.setElevation(
+ Double.parseDouble(mStringAccumulator.toString()));
+ } else if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setElevation(
+ Double.parseDouble(mStringAccumulator.toString()));
+ }
+ } else if (NODE_DESCRIPTION.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setDescription(mStringAccumulator.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ /**
+ * Converts the string description of the time into milliseconds since epoch.
+ * @param timeString the string data.
+ * @return date in milliseconds.
+ */
+ private long computeTime(String timeString) {
+ // Time looks like: 2008-04-05T19:24:50Z
+ Matcher m = ISO8601_TIME.matcher(timeString);
+ if (m.matches()) {
+ // get the various elements and reconstruct time as a long.
+ try {
+ int year = Integer.parseInt(m.group(1));
+ int month = Integer.parseInt(m.group(2));
+ int date = Integer.parseInt(m.group(3));
+ int hourOfDay = Integer.parseInt(m.group(4));
+ int minute = Integer.parseInt(m.group(5));
+ int second = Integer.parseInt(m.group(6));
+
+ // handle the optional parameters.
+ int milliseconds = 0;
+
+ String subSecondGroup = m.group(7);
+ if (subSecondGroup != null) {
+ milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup));
+ }
+
+ boolean utcTime = m.group(8) != null;
+
+ // now we convert into milliseconds since epoch.
+ Calendar c;
+ if (utcTime) {
+ c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
+ } else {
+ c = Calendar.getInstance();
+ }
+
+ c.set(year, month, date, hourOfDay, minute, second);
+
+ return c.getTimeInMillis() + milliseconds;
+ } catch (NumberFormatException e) {
+ // format is invalid, we'll return -1 below.
+ }
+
+ }
+
+ // invalid time!
+ return -1;
+ }
+
+ /**
+ * Handles the location attributes and store them into a {@link LocationPoint}.
+ * @param locationNode the {@link LocationPoint} to receive the location data.
+ * @param attributes the attributes from the XML node.
+ */
+ private void handleLocation(LocationPoint locationNode, Attributes attributes) {
+ try {
+ double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE));
+ double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE));
+
+ locationNode.setLocation(longitude, latitude);
+ } catch (NumberFormatException e) {
+ // wrong data, do nothing.
+ }
+ }
+
+ WayPoint[] getWayPoints() {
+ if (mWayPoints != null) {
+ return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+ }
+
+ return null;
+ }
+
+ Track[] getTracks() {
+ if (mTrackList != null) {
+ return mTrackList.toArray(new Track[mTrackList.size()]);
+ }
+
+ return null;
+ }
+
+ boolean getSuccess() {
+ return mSuccess;
+ }
+ }
+
+ /**
+ * A GPS track.
+ * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment.
+ */
+ public final static class Track {
+ private String mName;
+ private String mComment;
+ private List<TrackPoint> mPoints = new ArrayList<TrackPoint>();
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ void setComment(String comment) {
+ mComment = comment;
+ }
+
+ public String getComment() {
+ return mComment;
+ }
+
+ void addPoint(TrackPoint trackPoint) {
+ mPoints.add(trackPoint);
+ }
+
+ public TrackPoint[] getPoints() {
+ return mPoints.toArray(new TrackPoint[mPoints.size()]);
+ }
+
+ public long getFirstPointTime() {
+ if (mPoints.size() > 0) {
+ return mPoints.get(0).getTime();
+ }
+
+ return -1;
+ }
+
+ public long getLastPointTime() {
+ if (mPoints.size() > 0) {
+ return mPoints.get(mPoints.size()-1).getTime();
+ }
+
+ return -1;
+ }
+
+ public int getPointCount() {
+ return mPoints.size();
+ }
+ }
+
+ /**
+ * Creates a new GPX parser for a file specified by its full path.
+ * @param fileName The full path of the GPX file to parse.
+ */
+ public GpxParser(String fileName) {
+ mFileName = fileName;
+ }
+
+ /**
+ * Parses the GPX file.
+ * @return <code>true</code> if success.
+ */
+ public boolean parse() {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ mHandler = new GpxHandler();
+
+ parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+
+ return mHandler.getSuccess();
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public WayPoint[] getWayPoints() {
+ if (mHandler != null) {
+ return mHandler.getWayPoints();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public Track[] getTracks() {
+ if (mHandler != null) {
+ return mHandler.getTracks();
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java
new file mode 100644
index 0000000..af485ac
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic KML parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic Placemark information.
+ */
+public class KmlParser {
+
+ private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$
+
+ private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$
+ private final static String NODE_NAME = "name"; //$NON-NLS-1$
+ private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$
+
+ private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?");
+
+ private static SAXParserFactory sParserFactory;
+
+ static {
+ sParserFactory = SAXParserFactory.newInstance();
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ private String mFileName;
+
+ private KmlHandler mHandler;
+
+ /**
+ * Handler for the SAX parser.
+ */
+ private static class KmlHandler extends DefaultHandler {
+ // --------- parsed data ---------
+ List<WayPoint> mWayPoints;
+
+ // --------- state for parsing ---------
+ WayPoint mCurrentWayPoint;
+ final StringBuilder mStringAccumulator = new StringBuilder();
+
+ boolean mSuccess = true;
+
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ // we only care about the standard GPX nodes.
+ try {
+ if (uri.startsWith(NS_KML_2)) {
+ if (NODE_PLACEMARK.equals(localName)) {
+ if (mWayPoints == null) {
+ mWayPoints = new ArrayList<WayPoint>();
+ }
+
+ mWayPoints.add(mCurrentWayPoint = new WayPoint());
+ }
+ }
+ } finally {
+ // no matter the node, we empty the StringBuilder accumulator when we start
+ // a new node.
+ mStringAccumulator.setLength(0);
+ }
+ }
+
+ /**
+ * Processes new characters for the node content. The characters are simply stored,
+ * and will be processed when {@link #endElement(String, String, String)} is called.
+ */
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mStringAccumulator.append(ch, start, length);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+ if (uri.startsWith(NS_KML_2)) {
+ if (NODE_PLACEMARK.equals(localName)) {
+ mCurrentWayPoint = null;
+ } else if (NODE_NAME.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setName(mStringAccumulator.toString());
+ }
+ } else if (NODE_COORDINATES.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ parseLocation(mCurrentWayPoint, mStringAccumulator.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ /**
+ * Parses the location string and store the information into a {@link LocationPoint}.
+ * @param locationNode the {@link LocationPoint} to receive the location data.
+ * @param location The string containing the location info.
+ */
+ private void parseLocation(LocationPoint locationNode, String location) {
+ Matcher m = sLocationPattern.matcher(location);
+ if (m.matches()) {
+ try {
+ double longitude = Double.parseDouble(m.group(1));
+ double latitude = Double.parseDouble(m.group(2));
+
+ locationNode.setLocation(longitude, latitude);
+
+ if (m.groupCount() == 3) {
+ // looks like we have elevation data.
+ locationNode.setElevation(Double.parseDouble(m.group(3)));
+ }
+ } catch (NumberFormatException e) {
+ // wrong data, do nothing.
+ }
+ }
+ }
+
+ WayPoint[] getWayPoints() {
+ if (mWayPoints != null) {
+ return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+ }
+
+ return null;
+ }
+
+ boolean getSuccess() {
+ return mSuccess;
+ }
+ }
+
+ /**
+ * Creates a new GPX parser for a file specified by its full path.
+ * @param fileName The full path of the GPX file to parse.
+ */
+ public KmlParser(String fileName) {
+ mFileName = fileName;
+ }
+
+ /**
+ * Parses the GPX file.
+ * @return <code>true</code> if success.
+ */
+ public boolean parse() {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ mHandler = new KmlHandler();
+
+ parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+
+ return mHandler.getSuccess();
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public WayPoint[] getWayPoints() {
+ if (mHandler != null) {
+ return mHandler.getWayPoints();
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java
new file mode 100644
index 0000000..dbb8f41
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+/**
+ * Base class for Location aware points.
+ */
+class LocationPoint {
+ private double mLongitude;
+ private double mLatitude;
+ private boolean mHasElevation = false;
+ private double mElevation;
+
+ final void setLocation(double longitude, double latitude) {
+ mLongitude = longitude;
+ mLatitude = latitude;
+ }
+
+ public final double getLongitude() {
+ return mLongitude;
+ }
+
+ public final double getLatitude() {
+ return mLatitude;
+ }
+
+ final void setElevation(double elevation) {
+ mElevation = elevation;
+ mHasElevation = true;
+ }
+
+ public final boolean hasElevation() {
+ return mHasElevation;
+ }
+
+ public final double getElevation() {
+ return mElevation;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java
new file mode 100644
index 0000000..da21920
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link Track} objects in a Table.
+ * <p/>The expected type for the input is {@link Track}<code>[]</code>.
+ */
+public class TrackContentProvider implements IStructuredContentProvider {
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Track[]) {
+ return (Track[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java
new file mode 100644
index 0000000..50acb53
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link Track} objects.
+ */
+public class TrackLabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof Track) {
+ Track track = (Track)element;
+ switch (columnIndex) {
+ case 0:
+ return track.getName();
+ case 1:
+ return Integer.toString(track.getPointCount());
+ case 2:
+ long time = track.getFirstPointTime();
+ if (time != -1) {
+ return new Date(time).toString();
+ }
+ break;
+ case 3:
+ time = track.getLastPointTime();
+ if (time != -1) {
+ return new Date(time).toString();
+ }
+ break;
+ case 4:
+ return track.getComment();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java
new file mode 100644
index 0000000..527f4bf
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+
+/**
+ * A Track Point.
+ * <p/>A track point is a point in time and space.
+ */
+public class TrackPoint extends LocationPoint {
+ private long mTime;
+
+ void setTime(long time) {
+ mTime = time;
+ }
+
+ public long getTime() {
+ return mTime;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java
new file mode 100644
index 0000000..32880bd
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+/**
+ * A GPS/KML way point.
+ * <p/>A waypoint is a user specified location, with a name and an optional description.
+ */
+public final class WayPoint extends LocationPoint {
+ private String mName;
+ private String mDescription;
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ void setDescription(String description) {
+ mDescription = description;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java
new file mode 100644
index 0000000..1b7fe15
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link WayPoint} objects in a Table.
+ * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>.
+ */
+public class WayPointContentProvider implements IStructuredContentProvider {
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof WayPoint[]) {
+ return (WayPoint[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java
new file mode 100644
index 0000000..9f642f1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link WayPoint} objects.
+ */
+public class WayPointLabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)element;
+ switch (columnIndex) {
+ case 0:
+ return wayPoint.getName();
+ case 1:
+ return String.format("%.6f", wayPoint.getLongitude());
+ case 2:
+ return String.format("%.6f", wayPoint.getLatitude());
+ case 3:
+ if (wayPoint.hasElevation()) {
+ return String.format("%.1f", wayPoint.getElevation());
+ } else {
+ return "-";
+ }
+ case 4:
+ return wayPoint.getDescription();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java
new file mode 100644
index 0000000..da41e70
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class BugReportImporter {
+
+ private final static String TAG_HEADER = "------ EVENT LOG TAGS ------";
+ private final static String LOG_HEADER = "------ EVENT LOG ------";
+ private final static String HEADER_TAG = "------";
+
+ private String[] mTags;
+ private String[] mLog;
+
+ public BugReportImporter(String filePath) throws FileNotFoundException {
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(filePath)));
+
+ try {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (TAG_HEADER.equals(line)) {
+ readTags(reader);
+ return;
+ }
+ }
+ } catch (IOException e) {
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+
+ public String[] getTags() {
+ return mTags;
+ }
+
+ public String[] getLog() {
+ return mLog;
+ }
+
+ private void readTags(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ if (LOG_HEADER.equals(line)) {
+ mTags = content.toArray(new String[content.size()]);
+ readLog(reader);
+ return;
+ } else {
+ content.add(line);
+ }
+ }
+ }
+
+ private void readLog(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith(HEADER_TAG) == false) {
+ content.add(line);
+ } else {
+ break;
+ }
+ }
+
+ mLog = content.toArray(new String[content.size()]);
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java
new file mode 100644
index 0000000..473387a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+
+import java.util.ArrayList;
+
+public class DisplayFilteredLog extends DisplayLog {
+
+ public DisplayFilteredLog(String name) {
+ super(name);
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ ArrayList<ValueDisplayDescriptor> valueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+ addToLog(event, logParser, valueDescriptors, occurrenceDescriptors);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_FILTERED_LOG;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java
new file mode 100644
index 0000000..0cffd7e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DisplayGraph extends EventDisplay {
+
+ public DisplayGraph(String name) {
+ super(name);
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ Collection<TimeSeriesCollection> datasets = mValueTypeDataSetMap.values();
+ for (TimeSeriesCollection dataset : datasets) {
+ dataset.removeAllSeries();
+ }
+ if (mOccurrenceDataSet != null) {
+ mOccurrenceDataSet.removeAllSeries();
+ }
+ mValueDescriptorSeriesMap.clear();
+ mOcurrenceDescriptorSeriesMap.clear();
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ String title = getChartTitle(logParser);
+ return createCompositeChart(parent, logParser, title);
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ ArrayList<ValueDisplayDescriptor> valueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+ updateChart(event, logParser, valueDescriptors, occurrenceDescriptors);
+ }
+ }
+
+ /**
+ * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined
+ * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from
+ * the two lists.
+ * <p/>This method is only called when at least one of the descriptor list is non empty.
+ * @param event
+ * @param logParser
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ */
+ private void updateChart(EventContainer event, EventLogParser logParser,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+ Map<Integer, String> tagMap = logParser.getTagMap();
+
+ Millisecond millisecondTime = null;
+ long msec = -1;
+
+ // If the event container is a cpu container (tag == 2721), and there is no descriptor
+ // for the total CPU load, then we do accumulate all the values.
+ boolean accumulateValues = false;
+ double accumulatedValue = 0;
+
+ if (event.mTag == 2721) {
+ accumulateValues = true;
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ accumulateValues &= (descriptor.valueIndex != 0);
+ }
+ }
+
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ try {
+ // get the hashmap for this descriptor
+ HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor);
+
+ // if it's not there yet, we create it.
+ if (map == null) {
+ map = new HashMap<Integer, TimeSeries>();
+ mValueDescriptorSeriesMap.put(descriptor, map);
+ }
+
+ // get the TimeSeries for this pid
+ TimeSeries timeSeries = map.get(event.pid);
+
+ // if it doesn't exist yet, we create it
+ if (timeSeries == null) {
+ // get the series name
+ String seriesFullName = null;
+ String seriesLabel = getSeriesLabel(event, descriptor);
+
+ switch (mValueDescriptorCheck) {
+ case EVENT_CHECK_SAME_TAG:
+ seriesFullName = String.format("%1$s / %2$s", seriesLabel,
+ descriptor.valueName);
+ break;
+ case EVENT_CHECK_SAME_VALUE:
+ seriesFullName = String.format("%1$s", seriesLabel);
+ break;
+ default:
+ seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel,
+ tagMap.get(descriptor.eventTag),
+ descriptor.valueName);
+ break;
+ }
+
+ // get the data set for this ValueType
+ TimeSeriesCollection dataset = getValueDataset(
+ logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex]
+ .getValueType(),
+ accumulateValues);
+
+ // create the series
+ timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+ if (mMaximumChartItemAge != -1) {
+ timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000);
+ }
+
+ dataset.addSeries(timeSeries);
+
+ // add it to the map.
+ map.put(event.pid, timeSeries);
+ }
+
+ // update the timeSeries.
+
+ // get the value from the event
+ double value = event.getValueAsDouble(descriptor.valueIndex);
+
+ // accumulate the values if needed.
+ if (accumulateValues) {
+ accumulatedValue += value;
+ value = accumulatedValue;
+ }
+
+ // get the time
+ if (millisecondTime == null) {
+ msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ millisecondTime = new Millisecond(new Date(msec));
+ }
+
+ // add the value to the time series
+ timeSeries.addOrUpdate(millisecondTime, value);
+ } catch (InvalidTypeException e) {
+ // just ignore this descriptor if there's a type mismatch
+ }
+ }
+
+ for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) {
+ try {
+ // get the hashmap for this descriptor
+ HashMap<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor);
+
+ // if it's not there yet, we create it.
+ if (map == null) {
+ map = new HashMap<Integer, TimeSeries>();
+ mOcurrenceDescriptorSeriesMap.put(descriptor, map);
+ }
+
+ // get the TimeSeries for this pid
+ TimeSeries timeSeries = map.get(event.pid);
+
+ // if it doesn't exist yet, we create it.
+ if (timeSeries == null) {
+ String seriesLabel = getSeriesLabel(event, descriptor);
+
+ String seriesFullName = String.format("[%1$s:%2$s]",
+ tagMap.get(descriptor.eventTag), seriesLabel);
+
+ timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+ if (mMaximumChartItemAge != -1) {
+ timeSeries.setMaximumItemAge(mMaximumChartItemAge);
+ }
+
+ getOccurrenceDataSet().addSeries(timeSeries);
+
+ map.put(event.pid, timeSeries);
+ }
+
+ // update the series
+
+ // get the time
+ if (millisecondTime == null) {
+ msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ millisecondTime = new Millisecond(new Date(msec));
+ }
+
+ // add the value to the time series
+ timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused
+ } catch (InvalidTypeException e) {
+ // just ignore this descriptor if there's a type mismatch
+ }
+ }
+
+ // go through all the series and remove old values.
+ if (msec != -1 && mMaximumChartItemAge != -1) {
+ Collection<HashMap<Integer, TimeSeries>> pidMapValues =
+ mValueDescriptorSeriesMap.values();
+
+ for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+ Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+ for (TimeSeries timeSeries : seriesCollection) {
+ timeSeries.removeAgedItems(msec, true);
+ }
+ }
+
+ pidMapValues = mOcurrenceDescriptorSeriesMap.values();
+ for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+ Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+ for (TimeSeries timeSeries : seriesCollection) {
+ timeSeries.removeAgedItems(msec, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}.
+ * If the data set is not yet created, it is first allocated and set up into the
+ * {@link org.jfree.chart.JFreeChart} object.
+ * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set.
+ * @param accumulateValues
+ */
+ private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) {
+ TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type);
+ if (dataset == null) {
+ // create the data set and store it in the map
+ dataset = new TimeSeriesCollection();
+ mValueTypeDataSetMap.put(type, dataset);
+
+ // create the renderer and configure it depending on the ValueType
+ AbstractXYItemRenderer renderer;
+ if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) {
+ renderer = new XYAreaRenderer();
+ } else {
+ XYLineAndShapeRenderer r = new XYLineAndShapeRenderer();
+ r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT);
+
+ renderer = r;
+ }
+
+ // set both the dataset and the renderer in the plot object.
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setDataset(mDataSetCount, dataset);
+ xyPlot.setRenderer(mDataSetCount, renderer);
+
+ // put a new axis label, and configure it.
+ NumberAxis axis = new NumberAxis(type.toString());
+
+ if (type == EventValueDescription.ValueType.PERCENT) {
+ // force percent range to be (0,100) fixed.
+ axis.setAutoRange(false);
+ axis.setRange(0., 100.);
+ }
+
+ // for the index, we ignore the occurrence dataset
+ int count = mDataSetCount;
+ if (mOccurrenceDataSet != null) {
+ count--;
+ }
+
+ xyPlot.setRangeAxis(count, axis);
+ if ((count % 2) == 0) {
+ xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT);
+ } else {
+ xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT);
+ }
+
+ // now we link the dataset and the axis
+ xyPlot.mapDatasetToRangeAxis(mDataSetCount, count);
+
+ mDataSetCount++;
+ }
+
+ return dataset;
+ }
+
+ /**
+ * Return the series label for this event. This only contains the pid information.
+ * @param event the {@link EventContainer}
+ * @param descriptor the {@link OccurrenceDisplayDescriptor}
+ * @return the series label.
+ * @throws InvalidTypeException
+ */
+ private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor)
+ throws InvalidTypeException {
+ if (descriptor.seriesValueIndex != -1) {
+ if (descriptor.includePid == false) {
+ return event.getValueAsString(descriptor.seriesValueIndex);
+ } else {
+ return String.format("%1$s (%2$d)",
+ event.getValueAsString(descriptor.seriesValueIndex), event.pid);
+ }
+ }
+
+ return Integer.toString(event.pid);
+ }
+
+ /**
+ * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not
+ * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object.
+ */
+ private TimeSeriesCollection getOccurrenceDataSet() {
+ if (mOccurrenceDataSet == null) {
+ mOccurrenceDataSet = new TimeSeriesCollection();
+
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet);
+
+ OccurrenceRenderer renderer = new OccurrenceRenderer();
+ renderer.setBaseShapesVisible(false);
+ xyPlot.setRenderer(mDataSetCount, renderer);
+
+ mDataSetCount++;
+ }
+
+ return mOccurrenceDataSet;
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_GRAPH;
+ }
+
+ /**
+ * Sets the current {@link EventLogParser} object.
+ */
+ @Override
+ protected void setNewLogParser(EventLogParser logParser) {
+ if (mChart != null) {
+ mChart.setTitle(getChartTitle(logParser));
+ }
+ }
+ /**
+ * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}.
+ *
+ * @param logParser the logParser.
+ * @return the chart title.
+ */
+ private String getChartTitle(EventLogParser logParser) {
+ if (mValueDescriptors.size() > 0) {
+ String chartDesc = null;
+ switch (mValueDescriptorCheck) {
+ case EVENT_CHECK_SAME_TAG:
+ if (logParser != null) {
+ chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag);
+ }
+ break;
+ case EVENT_CHECK_SAME_VALUE:
+ if (logParser != null) {
+ chartDesc = String.format("%1$s / %2$s",
+ logParser.getTagMap().get(mValueDescriptors.get(0).eventTag),
+ mValueDescriptors.get(0).valueName);
+ }
+ break;
+ }
+
+ if (chartDesc != null) {
+ return String.format("%1$s - %2$s", mName, chartDesc);
+ }
+ }
+
+ return mName;
+ }
+} \ No newline at end of file
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java
new file mode 100644
index 0000000..8e7c1ac
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+public class DisplayLog extends EventDisplay {
+ public DisplayLog(String name) {
+ super(name);
+ }
+
+ private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$
+ private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$
+ private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$
+ private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$
+ private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$
+ private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ mLogTable.removeAll();
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ addToLog(event, logParser);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) {
+ return createLogUI(parent, listener);
+ }
+
+ /**
+ * Adds an {@link EventContainer} to the log.
+ *
+ * @param event the event.
+ * @param logParser the log parser.
+ */
+ private void addToLog(EventContainer event, EventLogParser logParser) {
+ ScrollBar bar = mLogTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // get the date.
+ Calendar c = Calendar.getInstance();
+ long msec = event.sec * 1000L;
+ c.setTimeInMillis(msec);
+
+ // convert the time into a string
+ String date = String.format("%1$tF %1$tT", c);
+
+ String eventName = logParser.getTagMap().get(event.mTag);
+ String pidName = Integer.toString(event.pid);
+
+ // get the value description
+ EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag);
+ if (valueDescription != null) {
+ for (int i = 0; i < valueDescription.length; i++) {
+ EventValueDescription description = valueDescription[i];
+ try {
+ String value = event.getValueAsString(i);
+
+ logValue(date, pidName, eventName, description.getName(), value,
+ description.getEventValueType(), description.getValueType());
+ } catch (InvalidTypeException e) {
+ logValue(date, pidName, eventName, description.getName(), e.getMessage(),
+ description.getEventValueType(), description.getValueType());
+ }
+ }
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ int itemCount = mLogTable.getItemCount();
+ if (itemCount > 0) {
+ mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by
+ * the list of descriptors. If an event is configured to be displayed by value and occurrence,
+ * only the values are displayed (as they mark an event occurrence anyway).
+ * <p/>This method is only called when at least one of the descriptor list is non empty.
+ *
+ * @param event
+ * @param logParser
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ */
+ protected void addToLog(EventContainer event, EventLogParser logParser,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+ ScrollBar bar = mLogTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // get the date.
+ Calendar c = Calendar.getInstance();
+ long msec = event.sec * 1000L;
+ c.setTimeInMillis(msec);
+
+ // convert the time into a string
+ String date = String.format("%1$tF %1$tT", c);
+
+ String eventName = logParser.getTagMap().get(event.mTag);
+ String pidName = Integer.toString(event.pid);
+
+ if (valueDescriptors.size() > 0) {
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ logDescriptor(event, descriptor, date, pidName, eventName, logParser);
+ }
+ } else {
+ // we display the event. Since the StringBuilder contains the header (date, event name,
+ // pid) at this point, there isn't anything else to display.
+ }
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ int itemCount = mLogTable.getItemCount();
+ if (itemCount > 0) {
+ mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+ }
+ }
+ }
+
+
+ /**
+ * Logs a value in the ui.
+ *
+ * @param date
+ * @param pid
+ * @param event
+ * @param valueName
+ * @param value
+ * @param eventValueType
+ * @param valueType
+ */
+ private void logValue(String date, String pid, String event, String valueName,
+ String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) {
+
+ TableItem item = new TableItem(mLogTable, SWT.NONE);
+ item.setText(0, date);
+ item.setText(1, pid);
+ item.setText(2, event);
+ item.setText(3, valueName);
+ item.setText(4, value);
+
+ String type;
+ if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) {
+ type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString());
+ } else {
+ type = eventValueType.toString();
+ }
+
+ item.setText(5, type);
+ }
+
+ /**
+ * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}.
+ *
+ * @param event the EventContainer
+ * @param descriptor the ValueDisplayDescriptor defining which value to display.
+ * @param date the date of the event in a string.
+ * @param pidName
+ * @param eventName
+ * @param logParser
+ */
+ private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor,
+ String date, String pidName, String eventName, EventLogParser logParser) {
+
+ String value;
+ try {
+ value = event.getValueAsString(descriptor.valueIndex);
+ } catch (InvalidTypeException e) {
+ value = e.getMessage();
+ }
+
+ EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag);
+
+ EventValueDescription valueDescription = values[descriptor.valueIndex];
+
+ logValue(date, pidName, eventName, descriptor.valueName, value,
+ valueDescription.getEventValueType(), valueDescription.getValueType());
+ }
+
+ /**
+ * Creates the UI for a log display.
+ *
+ * @param parent the parent {@link Composite}
+ * @param listener the {@link ILogColumnListener} to notify on column resize events.
+ * @return the top Composite of the UI.
+ */
+ private Control createLogUI(Composite parent, final ILogColumnListener listener) {
+ Composite mainComp = new Composite(parent, SWT.NONE);
+ GridLayout gl;
+ mainComp.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mainComp.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mLogTable = null;
+ }
+ });
+
+ Label l = new Label(mainComp, SWT.CENTER);
+ l.setText(mName);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL |
+ SWT.BORDER);
+ mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ TableColumn col = TableHelper.createTableColumn(
+ mLogTable, "Time",
+ SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(0, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "pid",
+ SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(1, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Event",
+ SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(2, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Name",
+ SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(3, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Value",
+ SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(4, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Type",
+ SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(5, (TableColumn) source);
+ }
+ }
+ });
+
+ mLogTable.setHeaderVisible(true);
+ mLogTable.setLinesVisible(true);
+
+ return mainComp;
+ }
+
+ /**
+ * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+ * <p/>
+ * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+ * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+ * column passed as argument.
+ *
+ * @param index the index of the column to resize
+ * @param sourceColumn the original column that was resize, and on which we need to sync the
+ * index-th column width.
+ */
+ @Override
+ void resizeColumn(int index, TableColumn sourceColumn) {
+ if (mLogTable != null) {
+ TableColumn col = mLogTable.getColumn(index);
+ if (col != sourceColumn) {
+ col.setWidth(sourceColumn.getWidth());
+ }
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_LOG_ALL;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java
new file mode 100644
index 0000000..6122513
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.FixedMillisecond;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.util.ShapeUtilities;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public class DisplaySync extends SyncCommon {
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasetsSync[];
+ private List<String> mTooltipsSync[];
+ private CustomXYToolTipGenerator mTooltipGenerators[];
+ private TimeSeries mDatasetsSyncTickle[];
+
+ // Dataset of error events to graph
+ private TimeSeries mDatasetError;
+
+ public DisplaySync(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Status");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+
+ XYBarRenderer br = new XYBarRenderer();
+ mDatasetsSync = new TimePeriodValues[NUM_AUTHS];
+
+ @SuppressWarnings("unchecked")
+ List<String> mTooltipsSyncTmp[] = new List[NUM_AUTHS];
+ mTooltipsSync = mTooltipsSyncTmp;
+
+ mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS];
+
+ TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(0, br);
+
+ XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer();
+ ls.setBaseLinesVisible(false);
+ mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS];
+ TimeSeriesCollection tsc = new TimeSeriesCollection();
+ xyPlot.setDataset(1, tsc);
+ xyPlot.setRenderer(1, ls);
+
+ mDatasetError = new TimeSeries("Errors", FixedMillisecond.class);
+ xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError));
+ XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer();
+ errls.setBaseLinesVisible(false);
+ errls.setSeriesPaint(0, Color.RED);
+ xyPlot.setRenderer(2, errls);
+
+ for (int i = 0; i < NUM_AUTHS; i++) {
+ br.setSeriesPaint(i, AUTH_COLORS[i]);
+ ls.setSeriesPaint(i, AUTH_COLORS[i]);
+ mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]);
+ tpvc.addSeries(mDatasetsSync[i]);
+ mTooltipsSync[i] = new ArrayList<String>();
+ mTooltipGenerators[i] = new CustomXYToolTipGenerator();
+ br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]);
+ mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]);
+
+ mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle",
+ FixedMillisecond.class);
+ tsc.addSeries(mDatasetsSyncTickle[i]);
+ ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f));
+ }
+ }
+
+ /**
+ * Updates the display with a new event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ super.newEvent(event, logParser); // Handle sync operation
+ try {
+ if (event.mTag == EVENT_TICKLE) {
+ int auth = getAuth(event.getValueAsString(0));
+ if (auth >= 0) {
+ long msec = event.sec * 1000L + (event.nsec / 1000000L);
+ mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1);
+ }
+ }
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Generate the height for an event.
+ * Height is somewhat arbitrarily the count of "things" that happened
+ * during the sync.
+ * When network traffic measurements are available, code should be modified
+ * to use that instead.
+ * @param details The details string associated with the event
+ * @return The height in arbirary units (0-100)
+ */
+ private int getHeightFromDetails(String details) {
+ if (details == null) {
+ return 1; // Arbitrary
+ }
+ int total = 0;
+ String parts[] = details.split("[a-zA-Z]");
+ for (String part : parts) {
+ if ("".equals(part)) continue;
+ total += Integer.parseInt(part);
+ }
+ if (total == 0) {
+ total = 1;
+ }
+ return total;
+ }
+
+ /**
+ * Generates the tooltips text for an event.
+ * This method decodes the cryptic details string.
+ * @param auth The authority associated with the event
+ * @param details The details string
+ * @param eventSource server, poll, etc.
+ * @return The text to display in the tooltips
+ */
+ private String getTextFromDetails(int auth, String details, int eventSource) {
+
+ StringBuffer sb = new StringBuffer();
+ sb.append(AUTH_NAMES[auth]).append(": \n");
+
+ Scanner scanner = new Scanner(details);
+ Pattern charPat = Pattern.compile("[a-zA-Z]");
+ Pattern numPat = Pattern.compile("[0-9]+");
+ while (scanner.hasNext()) {
+ String key = scanner.findInLine(charPat);
+ int val = Integer.parseInt(scanner.findInLine(numPat));
+ if (auth == GMAIL && "M".equals(key)) {
+ sb.append("messages from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "L".equals(key)) {
+ sb.append("labels from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "C".equals(key)) {
+ sb.append("check conversation requests from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "A".equals(key)) {
+ sb.append("attachments from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "U".equals(key)) {
+ sb.append("op updates from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "u".equals(key)) {
+ sb.append("op updates to server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "S".equals(key)) {
+ sb.append("send/receive cycles: ").append(val).append("\n");
+ } else if ("Q".equals(key)) {
+ sb.append("queries to server: ").append(val).append("\n");
+ } else if ("E".equals(key)) {
+ sb.append("entries from server: ").append(val).append("\n");
+ } else if ("u".equals(key)) {
+ sb.append("updates from client: ").append(val).append("\n");
+ } else if ("i".equals(key)) {
+ sb.append("inserts from client: ").append(val).append("\n");
+ } else if ("d".equals(key)) {
+ sb.append("deletes from client: ").append(val).append("\n");
+ } else if ("f".equals(key)) {
+ sb.append("full sync requested\n");
+ } else if ("r".equals(key)) {
+ sb.append("partial sync unavailable\n");
+ } else if ("X".equals(key)) {
+ sb.append("hard error\n");
+ } else if ("e".equals(key)) {
+ sb.append("number of parse exceptions: ").append(val).append("\n");
+ } else if ("c".equals(key)) {
+ sb.append("number of conflicts: ").append(val).append("\n");
+ } else if ("a".equals(key)) {
+ sb.append("number of auth exceptions: ").append(val).append("\n");
+ } else if ("D".equals(key)) {
+ sb.append("too many deletions\n");
+ } else if ("R".equals(key)) {
+ sb.append("too many retries: ").append(val).append("\n");
+ } else if ("b".equals(key)) {
+ sb.append("database error\n");
+ } else if ("x".equals(key)) {
+ sb.append("soft error\n");
+ } else if ("l".equals(key)) {
+ sb.append("sync already in progress\n");
+ } else if ("I".equals(key)) {
+ sb.append("io exception\n");
+ } else if (auth == CONTACTS && "g".equals(key)) {
+ sb.append("aggregation query: ").append(val).append("\n");
+ } else if (auth == CONTACTS && "G".equals(key)) {
+ sb.append("aggregation merge: ").append(val).append("\n");
+ } else if (auth == CONTACTS && "n".equals(key)) {
+ sb.append("num entries: ").append(val).append("\n");
+ } else if (auth == CONTACTS && "p".equals(key)) {
+ sb.append("photos uploaded from server: ").append(val).append("\n");
+ } else if (auth == CONTACTS && "P".equals(key)) {
+ sb.append("photos downloaded from server: ").append(val).append("\n");
+ } else if (auth == CALENDAR && "F".equals(key)) {
+ sb.append("server refresh\n");
+ } else if (auth == CALENDAR && "s".equals(key)) {
+ sb.append("server diffs fetched\n");
+ } else {
+ sb.append(key).append("=").append(val);
+ }
+ }
+ if (eventSource == 0) {
+ sb.append("(server)");
+ } else if (eventSource == 1) {
+ sb.append("(local)");
+ } else if (eventSource == 2) {
+ sb.append("(poll)");
+ } else if (eventSource == 3) {
+ sb.append("(user)");
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Callback to process a sync event.
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (!newEvent) {
+ // Details arrived for a previous sync event
+ // Remove event before reinserting.
+ int lastItem = mDatasetsSync[auth].getItemCount();
+ mDatasetsSync[auth].delete(lastItem-1, lastItem-1);
+ mTooltipsSync[auth].remove(lastItem-1);
+ }
+ double height = getHeightFromDetails(details);
+ height = height / (stopTime - startTime + 1) * 10000;
+ if (height > 30) {
+ height = 30;
+ }
+ mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height);
+ mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource));
+ mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]);
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ long msec = event.sec * 1000L + (event.nsec / 1000000L);
+ mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
new file mode 100644
index 0000000..5bfc039
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.RegularTimePeriod;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class DisplaySyncHistogram extends SyncCommon {
+
+ Map<SimpleTimePeriod, Integer> mTimePeriodMap[];
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasetsSyncHist[];
+
+ public DisplaySyncHistogram(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Histogram");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+
+ AbstractXYItemRenderer br = new XYBarRenderer();
+ mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1];
+
+ @SuppressWarnings("unchecked")
+ Map<SimpleTimePeriod, Integer> mTimePeriodMapTmp[] = new HashMap[NUM_AUTHS + 1];
+ mTimePeriodMap = mTimePeriodMapTmp;
+
+ TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(br);
+
+ for (int i = 0; i < NUM_AUTHS + 1; i++) {
+ br.setSeriesPaint(i, AUTH_COLORS[i]);
+ mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]);
+ tpvc.addSeries(mDatasetsSyncHist[i]);
+ mTimePeriodMap[i] = new HashMap<SimpleTimePeriod, Integer>();
+
+ }
+ }
+
+ /**
+ * Callback to process a sync event.
+ *
+ * @param event The sync event
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (newEvent) {
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ auth = ERRORS;
+ }
+ double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+ addHistEvent(0, auth, delta);
+ } else {
+ // sync_details arrived for an event that has already been graphed.
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ // Item turns out to be in error, so transfer time from old auth to error.
+ double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+ addHistEvent(0, auth, -delta);
+ addHistEvent(0, ERRORS, delta);
+ }
+ }
+ }
+
+ /**
+ * Helper to add an event to the data series.
+ * Also updates error series if appropriate (x or X in details).
+ * @param stopTime Time event ends
+ * @param auth Sync authority
+ * @param value Value to graph for event
+ */
+ private void addHistEvent(long stopTime, int auth, double value) {
+ SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth);
+
+ // Loop over all datasets to do the stacking.
+ for (int i = auth; i <= ERRORS; i++) {
+ addToPeriod(mDatasetsSyncHist, i, hour, value);
+ }
+ }
+
+ private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period,
+ double value) {
+ int index;
+ if (mTimePeriodMap[auth].containsKey(period)) {
+ index = mTimePeriodMap[auth].get(period);
+ double oldValue = tpv[auth].getValue(index).doubleValue();
+ tpv[auth].update(index, oldValue + value);
+ } else {
+ index = tpv[auth].getItemCount();
+ mTimePeriodMap[auth].put(period, index);
+ tpv[auth].add(period, value);
+ }
+ }
+
+ /**
+ * Creates a multiple-hour time period for the histogram.
+ * @param time Time in milliseconds.
+ * @param numHoursWide: should divide into a day.
+ * @return SimpleTimePeriod covering the number of hours and containing time.
+ */
+ private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) {
+ Date date = new Date(time);
+ TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE;
+ Calendar calendar = Calendar.getInstance(zone);
+ calendar.setTime(date);
+ long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) +
+ calendar.get(Calendar.DAY_OF_YEAR) * 24;
+ int year = calendar.get(Calendar.YEAR);
+ hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide;
+ calendar.clear();
+ calendar.set(year, 0, 1, 0, 0); // Jan 1
+ long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000;
+ return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000);
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC_HIST;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java
new file mode 100644
index 0000000..10176e3
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DisplaySyncPerf extends SyncCommon {
+
+ CustomXYToolTipGenerator mTooltipGenerator;
+
+ List<String> mTooltips[];
+
+ // The series number for each graphed item.
+ // sync authorities are 0-3
+ private static final int DB_QUERY = 4;
+ private static final int DB_WRITE = 5;
+ private static final int HTTP_NETWORK = 6;
+ private static final int HTTP_PROCESSING = 7;
+ private static final int NUM_SERIES = (HTTP_PROCESSING + 1);
+ private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+ "DB Query", "DB Write", "HTTP Response", "HTTP Processing",};
+ private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+ Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY};
+ private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2};
+
+ // Values from data/etc/event-log-tags
+ private static final int EVENT_DB_OPERATION = 52000;
+ private static final int EVENT_HTTP_STATS = 52001;
+ // op types for EVENT_DB_OPERATION
+ final int EVENT_DB_QUERY = 0;
+ final int EVENT_DB_WRITE = 1;
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasets[];
+
+ /**
+ * TimePeriodValuesCollection that supports Y intervals. This allows the
+ * creation of "floating" bars, rather than bars rooted to the axis.
+ */
+ class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection {
+ /** default serial UID */
+ private static final long serialVersionUID = 1L;
+
+ private double yheight;
+
+ /**
+ * Constructs a collection of bars with a fixed Y height.
+ *
+ * @param yheight The height of the bars.
+ */
+ YIntervalTimePeriodValuesCollection(double yheight) {
+ this.yheight = yheight;
+ }
+
+ /**
+ * Returns ending Y value that is a fixed amount greater than the starting value.
+ *
+ * @param series the series (zero-based index).
+ * @param item the item (zero-based index).
+ * @return The ending Y value for the specified series and item.
+ */
+ @Override
+ public Number getEndY(int series, int item) {
+ return getY(series, item).doubleValue() + yheight;
+ }
+ }
+
+ /**
+ * Constructs a graph of network and database stats.
+ *
+ * @param name The name of this graph in the graph list.
+ */
+ public DisplaySyncPerf(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Performance");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.getRangeAxis().setVisible(false);
+ mTooltipGenerator = new CustomXYToolTipGenerator();
+
+ @SuppressWarnings("unchecked")
+ List<String>[] mTooltipsTmp = new List[NUM_SERIES];
+ mTooltips = mTooltipsTmp;
+
+ XYBarRenderer br = new XYBarRenderer();
+ br.setUseYInterval(true);
+ mDatasets = new TimePeriodValues[NUM_SERIES];
+
+ TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1);
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(br);
+
+ for (int i = 0; i < NUM_SERIES; i++) {
+ br.setSeriesPaint(i, SERIES_COLORS[i]);
+ mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]);
+ tpvc.addSeries(mDatasets[i]);
+ mTooltips[i] = new ArrayList<String>();
+ mTooltipGenerator.addToolTipSeries(mTooltips[i]);
+ br.setSeriesToolTipGenerator(i, mTooltipGenerator);
+ }
+ }
+
+ /**
+ * Updates the display with a new event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ super.newEvent(event, logParser); // Handle sync operation
+ try {
+ if (event.mTag == EVENT_DB_OPERATION) {
+ // 52000 db_operation (name|3),(op_type|1|5),(time|2|3)
+ String tip = event.getValueAsString(0);
+ long endTime = event.sec * 1000L + (event.nsec / 1000000L);
+ int opType = Integer.parseInt(event.getValueAsString(1));
+ long duration = Long.parseLong(event.getValueAsString(2));
+
+ if (opType == EVENT_DB_QUERY) {
+ mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime),
+ SERIES_YCOORD[DB_QUERY]);
+ mTooltips[DB_QUERY].add(tip);
+ } else if (opType == EVENT_DB_WRITE) {
+ mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime),
+ SERIES_YCOORD[DB_WRITE]);
+ mTooltips[DB_WRITE].add(tip);
+ }
+ } else if (event.mTag == EVENT_HTTP_STATS) {
+ // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2)
+ String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) +
+ ", rx: " + event.getValueAsString(4);
+ long endTime = event.sec * 1000L + (event.nsec / 1000000L);
+ long netEndTime = endTime - Long.parseLong(event.getValueAsString(2));
+ long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1));
+ mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime),
+ SERIES_YCOORD[HTTP_NETWORK]);
+ mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime),
+ SERIES_YCOORD[HTTP_PROCESSING]);
+ mTooltips[HTTP_NETWORK].add(tip);
+ mTooltips[HTTP_PROCESSING].add(tip);
+ }
+ } catch (NumberFormatException e) {
+ // This can happen when parsing events from froyo+ where the event with id 52000
+ // as a completely different format. For now, skip this event if this happens.
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Callback from super.newEvent to process a sync event.
+ *
+ * @param event The sync event
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (newEvent) {
+ mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC_PERF;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java
new file mode 100644
index 0000000..d0d2789
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java
@@ -0,0 +1,975 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.event.ChartChangeEvent;
+import org.jfree.chart.event.ChartChangeEventType;
+import org.jfree.chart.event.ChartChangeListener;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a custom display of one or more events.
+ */
+abstract class EventDisplay {
+
+ private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$
+ private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$
+ private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$
+ private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$
+
+ private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$
+
+ public final static int DISPLAY_TYPE_LOG_ALL = 0;
+ public final static int DISPLAY_TYPE_FILTERED_LOG = 1;
+ public final static int DISPLAY_TYPE_GRAPH = 2;
+ public final static int DISPLAY_TYPE_SYNC = 3;
+ public final static int DISPLAY_TYPE_SYNC_HIST = 4;
+ public final static int DISPLAY_TYPE_SYNC_PERF = 5;
+
+ private final static int EVENT_CHECK_FAILED = 0;
+ protected final static int EVENT_CHECK_SAME_TAG = 1;
+ protected final static int EVENT_CHECK_SAME_VALUE = 2;
+
+ /**
+ * Creates the appropriate EventDisplay subclass.
+ *
+ * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc)
+ * @param name the name of the display
+ * @return the created object
+ */
+ public static EventDisplay eventDisplayFactory(int type, String name) {
+ switch (type) {
+ case DISPLAY_TYPE_LOG_ALL:
+ return new DisplayLog(name);
+ case DISPLAY_TYPE_FILTERED_LOG:
+ return new DisplayFilteredLog(name);
+ case DISPLAY_TYPE_SYNC:
+ return new DisplaySync(name);
+ case DISPLAY_TYPE_SYNC_HIST:
+ return new DisplaySyncHistogram(name);
+ case DISPLAY_TYPE_GRAPH:
+ return new DisplayGraph(name);
+ case DISPLAY_TYPE_SYNC_PERF:
+ return new DisplaySyncPerf(name);
+ default:
+ throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Adds event to the display.
+ * @param event The event
+ * @param logParser The log parser.
+ */
+ abstract void newEvent(EventContainer event, EventLogParser logParser);
+
+ /**
+ * Resets the display.
+ */
+ abstract void resetUI();
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ abstract int getDisplayType();
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ abstract Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener);
+
+ interface ILogColumnListener {
+ void columnResized(int index, TableColumn sourceColumn);
+ }
+
+ /**
+ * Describes an event to be displayed.
+ */
+ static class OccurrenceDisplayDescriptor {
+
+ int eventTag = -1;
+ int seriesValueIndex = -1;
+ boolean includePid = false;
+ int filterValueIndex = -1;
+ CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO;
+ Object filterValue = null;
+
+ OccurrenceDisplayDescriptor() {
+ }
+
+ OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) {
+ replaceWith(descriptor);
+ }
+
+ OccurrenceDisplayDescriptor(int eventTag) {
+ this.eventTag = eventTag;
+ }
+
+ OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) {
+ this.eventTag = eventTag;
+ this.seriesValueIndex = seriesValueIndex;
+ }
+
+ void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+ eventTag = descriptor.eventTag;
+ seriesValueIndex = descriptor.seriesValueIndex;
+ includePid = descriptor.includePid;
+ filterValueIndex = descriptor.filterValueIndex;
+ filterCompareMethod = descriptor.filterCompareMethod;
+ filterValue = descriptor.filterValue;
+ }
+
+ /**
+ * Loads the descriptor parameter from a storage string. The storage string must have
+ * been generated with {@link #getStorageString()}.
+ *
+ * @param storageString the storage string
+ */
+ final void loadFrom(String storageString) {
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR));
+ loadFrom(values, 0);
+ }
+
+ /**
+ * Loads the parameters from an array of strings.
+ *
+ * @param storageStrings the strings representing each parameter.
+ * @param index the starting index in the array of strings.
+ * @return the new index in the array.
+ */
+ protected int loadFrom(String[] storageStrings, int index) {
+ eventTag = Integer.parseInt(storageStrings[index++]);
+ seriesValueIndex = Integer.parseInt(storageStrings[index++]);
+ includePid = Boolean.parseBoolean(storageStrings[index++]);
+ filterValueIndex = Integer.parseInt(storageStrings[index++]);
+ try {
+ filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]);
+ } catch (IllegalArgumentException e) {
+ // if the name does not match any known CompareMethod, we init it to the default one
+ filterCompareMethod = CompareMethod.EQUAL_TO;
+ }
+ String value = storageStrings[index++];
+ if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) {
+ filterValue = EventValueType.getObjectFromStorageString(value);
+ }
+
+ return index;
+ }
+
+ /**
+ * Returns the storage string for the receiver.
+ */
+ String getStorageString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(eventTag);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(seriesValueIndex);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(Boolean.toString(includePid));
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(filterValueIndex);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(filterCompareMethod.name());
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ if (filterValue != null) {
+ String value = EventValueType.getStorageString(filterValue);
+ if (value != null) {
+ sb.append(value);
+ } else {
+ sb.append(FILTER_VALUE_NULL);
+ }
+ } else {
+ sb.append(FILTER_VALUE_NULL);
+ }
+
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Describes an event value to be displayed.
+ */
+ static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor {
+ String valueName;
+ int valueIndex = -1;
+
+ ValueDisplayDescriptor() {
+ super();
+ }
+
+ ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) {
+ super();
+ replaceWith(descriptor);
+ }
+
+ ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) {
+ super(eventTag);
+ this.valueName = valueName;
+ this.valueIndex = valueIndex;
+ }
+
+ ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex,
+ int seriesValueIndex) {
+ super(eventTag, seriesValueIndex);
+ this.valueName = valueName;
+ this.valueIndex = valueIndex;
+ }
+
+ @Override
+ void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+ super.replaceWith(descriptor);
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor;
+ valueName = valueDescriptor.valueName;
+ valueIndex = valueDescriptor.valueIndex;
+ }
+ }
+
+ /**
+ * Loads the parameters from an array of strings.
+ *
+ * @param storageStrings the strings representing each parameter.
+ * @param index the starting index in the array of strings.
+ * @return the new index in the array.
+ */
+ @Override
+ protected int loadFrom(String[] storageStrings, int index) {
+ index = super.loadFrom(storageStrings, index);
+ valueName = storageStrings[index++];
+ valueIndex = Integer.parseInt(storageStrings[index++]);
+ return index;
+ }
+
+ /**
+ * Returns the storage string for the receiver.
+ */
+ @Override
+ String getStorageString() {
+ String superStorage = super.getStorageString();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(superStorage);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(valueName);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(valueIndex);
+
+ return sb.toString();
+ }
+ }
+
+ /* ==================
+ * Event Display parameters.
+ * ================== */
+ protected String mName;
+
+ private boolean mPidFiltering = false;
+
+ private ArrayList<Integer> mPidFilterList = null;
+
+ protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+ private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ /* ==================
+ * Event Display members for display purpose.
+ * ================== */
+ // chart objects
+ /**
+ * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+ */
+ protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap =
+ new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+ /**
+ * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+ */
+ protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap =
+ new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+
+ /**
+ * This is a map of (ValueType, dataset)
+ */
+ protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap =
+ new HashMap<ValueType, TimeSeriesCollection>();
+
+ protected JFreeChart mChart;
+ protected TimeSeriesCollection mOccurrenceDataSet;
+ protected int mDataSetCount;
+ private ChartComposite mChartComposite;
+ protected long mMaximumChartItemAge = -1;
+ protected long mHistWidth = 1;
+
+ // log objects.
+ protected Table mLogTable;
+
+ /* ==================
+ * Misc data.
+ * ================== */
+ protected int mValueDescriptorCheck = EVENT_CHECK_FAILED;
+
+ EventDisplay(String name) {
+ mName = name;
+ }
+
+ static EventDisplay clone(EventDisplay from) {
+ EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName());
+ ed.mName = from.mName;
+ ed.mPidFiltering = from.mPidFiltering;
+ ed.mMaximumChartItemAge = from.mMaximumChartItemAge;
+ ed.mHistWidth = from.mHistWidth;
+
+ if (from.mPidFilterList != null) {
+ ed.mPidFilterList = new ArrayList<Integer>();
+ ed.mPidFilterList.addAll(from.mPidFilterList);
+ }
+
+ for (ValueDisplayDescriptor desc : from.mValueDescriptors) {
+ ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc));
+ }
+ ed.mValueDescriptorCheck = from.mValueDescriptorCheck;
+
+ for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) {
+ ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc));
+ }
+ return ed;
+ }
+
+ /**
+ * Returns the parameters of the receiver as a single String for storage.
+ */
+ String getStorageString() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(mName);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDisplayType());
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(Boolean.toString(mPidFiltering));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getPidStorageString());
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDescriptorStorageString(mValueDescriptors));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDescriptorStorageString(mOccurrenceDescriptors));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(mMaximumChartItemAge);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(mHistWidth);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+
+ return sb.toString();
+ }
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ String getName() {
+ return mName;
+ }
+
+ void setPidFiltering(boolean filterByPid) {
+ mPidFiltering = filterByPid;
+ }
+
+ boolean getPidFiltering() {
+ return mPidFiltering;
+ }
+
+ void setPidFilterList(ArrayList<Integer> pids) {
+ if (mPidFiltering == false) {
+ throw new InvalidParameterException();
+ }
+
+ mPidFilterList = pids;
+ }
+
+ ArrayList<Integer> getPidFilterList() {
+ return mPidFilterList;
+ }
+
+ void addPidFiler(int pid) {
+ if (mPidFiltering == false) {
+ throw new InvalidParameterException();
+ }
+
+ if (mPidFilterList == null) {
+ mPidFilterList = new ArrayList<Integer>();
+ }
+
+ mPidFilterList.add(pid);
+ }
+
+ /**
+ * Returns an iterator to the list of {@link ValueDisplayDescriptor}.
+ */
+ Iterator<ValueDisplayDescriptor> getValueDescriptors() {
+ return mValueDescriptors.iterator();
+ }
+
+ /**
+ * Update checks on the descriptors. Must be called whenever a descriptor is modified outside
+ * of this class.
+ */
+ void updateValueDescriptorCheck() {
+ mValueDescriptorCheck = checkDescriptors();
+ }
+
+ /**
+ * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}.
+ */
+ Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() {
+ return mOccurrenceDescriptors.iterator();
+ }
+
+ /**
+ * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a
+ * {@link ValueDisplayDescriptor}.
+ *
+ * @param descriptor the descriptor to be added.
+ */
+ void addDescriptor(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ mValueDescriptors.add((ValueDisplayDescriptor) descriptor);
+ mValueDescriptorCheck = checkDescriptors();
+ } else {
+ mOccurrenceDescriptors.add(descriptor);
+ }
+ }
+
+ /**
+ * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}).
+ *
+ * @param descriptorClass the class of the descriptor to return.
+ * @param index the index of the descriptor to return.
+ * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor}
+ * or <code>null</code> if <code>descriptorClass</code> is another class.
+ */
+ OccurrenceDisplayDescriptor getDescriptor(
+ Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+
+ if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+ return mOccurrenceDescriptors.get(index);
+ } else if (descriptorClass == ValueDisplayDescriptor.class) {
+ return mValueDescriptors.get(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes a descriptor based on its class and index.
+ *
+ * @param descriptorClass the class of the descriptor.
+ * @param index the index of the descriptor to be removed.
+ */
+ void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+ if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+ mOccurrenceDescriptors.remove(index);
+ } else if (descriptorClass == ValueDisplayDescriptor.class) {
+ mValueDescriptors.remove(index);
+ mValueDescriptorCheck = checkDescriptors();
+ }
+ }
+
+ Control createCompositeChart(final Composite parent, EventLogParser logParser,
+ String title) {
+ mChart = ChartFactory.createTimeSeriesChart(
+ null,
+ null /* timeAxisLabel */,
+ null /* valueAxisLabel */,
+ null, /* dataset. set below */
+ true /* legend */,
+ false /* tooltips */,
+ false /* urls */);
+
+ // get the font to make a proper title. We need to convert the swt font,
+ // into an awt font.
+ Font f = parent.getFont();
+ FontData[] fData = f.getFontData();
+
+ // event though on Mac OS there could be more than one fontData, we'll only use
+ // the first one.
+ FontData firstFontData = fData[0];
+
+ java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(),
+ firstFontData, true /* ensureSameSize */);
+
+
+ mChart.setTitle(new TextTitle(title, awtFont));
+
+ final XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setRangeCrosshairVisible(true);
+ xyPlot.setRangeCrosshairLockedOnData(true);
+ xyPlot.setDomainCrosshairVisible(true);
+ xyPlot.setDomainCrosshairLockedOnData(true);
+
+ mChart.addChangeListener(new ChartChangeListener() {
+ @Override
+ public void chartChanged(ChartChangeEvent event) {
+ ChartChangeEventType type = event.getType();
+ if (type == ChartChangeEventType.GENERAL) {
+ // because the value we need (rangeCrosshair and domainCrosshair) are
+ // updated on the draw, but the notification happens before the draw,
+ // we process the click in a future runnable!
+ parent.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ processClick(xyPlot);
+ }
+ });
+ }
+ }
+ });
+
+ mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000, // max draw width. We don't want it to zoom, so we put a big number
+ 3000, // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ true, // zoom
+ true); // tooltips
+
+ mChartComposite.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mValueTypeDataSetMap.clear();
+ mDataSetCount = 0;
+ mOccurrenceDataSet = null;
+ mChart = null;
+ mChartComposite = null;
+ mValueDescriptorSeriesMap.clear();
+ mOcurrenceDescriptorSeriesMap.clear();
+ }
+ });
+
+ return mChartComposite;
+
+ }
+
+ private void processClick(XYPlot xyPlot) {
+ double rangeValue = xyPlot.getRangeCrosshairValue();
+ if (rangeValue != 0) {
+ double domainValue = xyPlot.getDomainCrosshairValue();
+
+ Millisecond msec = new Millisecond(new Date((long) domainValue));
+
+ // look for values in the dataset that contains data at this TimePeriod
+ Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet();
+
+ for (ValueDisplayDescriptor descKey : descKeys) {
+ HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey);
+
+ Set<Integer> pidKeys = map.keySet();
+
+ for (Integer pidKey : pidKeys) {
+ TimeSeries series = map.get(pidKey);
+
+ Number value = series.getValue(msec);
+ if (value != null) {
+ // found a match. lets check against the actual value.
+ if (value.doubleValue() == rangeValue) {
+
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+ * Subclasses can override if necessary.
+ * <p/>
+ * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+ * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+ * column passed as argument.
+ *
+ * @param index the index of the column to resize
+ * @param sourceColumn the original column that was resize, and on which we need to sync the
+ * index-th column width.
+ */
+ void resizeColumn(int index, TableColumn sourceColumn) {
+ }
+
+ /**
+ * Sets the current {@link EventLogParser} object.
+ * Subclasses can override if necessary.
+ */
+ protected void setNewLogParser(EventLogParser logParser) {
+ }
+
+ /**
+ * Prepares the {@link EventDisplay} for a multi event display.
+ */
+ void startMultiEventDisplay() {
+ if (mLogTable != null) {
+ mLogTable.setRedraw(false);
+ }
+ }
+
+ /**
+ * Finalizes the {@link EventDisplay} after a multi event display.
+ */
+ void endMultiEventDisplay() {
+ if (mLogTable != null) {
+ mLogTable.setRedraw(true);
+ }
+ }
+
+ /**
+ * Returns the {@link Table} object used to display events, if any.
+ *
+ * @return a Table object or <code>null</code>.
+ */
+ Table getTable() {
+ return mLogTable;
+ }
+
+ /**
+ * Loads a new {@link EventDisplay} from a storage string. The string must have been created
+ * with {@link #getStorageString()}.
+ *
+ * @param storageString the storage string
+ * @return a new {@link EventDisplay} or null if the load failed.
+ */
+ static EventDisplay load(String storageString) {
+ if (storageString.length() > 0) {
+ // the storage string is separated by ':'
+ String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR));
+
+ try {
+ int index = 0;
+
+ String name = values[index++];
+ int displayType = Integer.parseInt(values[index++]);
+ boolean pidFiltering = Boolean.parseBoolean(values[index++]);
+
+ EventDisplay ed = eventDisplayFactory(displayType, name);
+ ed.setPidFiltering(pidFiltering);
+
+ // because empty sections are removed by String.split(), we have to check
+ // the index for those.
+ if (index < values.length) {
+ ed.loadPidFilters(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.loadValueDescriptors(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.loadOccurrenceDescriptors(values[index++]);
+ }
+
+ ed.updateValueDescriptorCheck();
+
+ if (index < values.length) {
+ ed.mMaximumChartItemAge = Long.parseLong(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.mHistWidth = Long.parseLong(values[index++]);
+ }
+
+ return ed;
+ } catch (RuntimeException re) {
+ // we'll return null below.
+ Log.e("ddms", re);
+ }
+ }
+
+ return null;
+ }
+
+ private String getPidStorageString() {
+ if (mPidFilterList != null) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (Integer i : mPidFilterList) {
+ if (first == false) {
+ sb.append(PID_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+ sb.append(i);
+ }
+
+ return sb.toString();
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+
+ private void loadPidFilters(String storageString) {
+ if (storageString.length() > 0) {
+ String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ if (mPidFilterList == null) {
+ mPidFilterList = new ArrayList<Integer>();
+ }
+ mPidFilterList.add(Integer.parseInt(value));
+ }
+ }
+ }
+
+ private String getDescriptorStorageString(
+ ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+
+ for (OccurrenceDisplayDescriptor descriptor : descriptorList) {
+ if (first == false) {
+ sb.append(DESCRIPTOR_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+ sb.append(descriptor.getStorageString());
+ }
+
+ return sb.toString();
+ }
+
+ private void loadOccurrenceDescriptors(String storageString) {
+ if (storageString.length() == 0) {
+ return;
+ }
+
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor();
+ desc.loadFrom(value);
+ mOccurrenceDescriptors.add(desc);
+ }
+ }
+
+ private void loadValueDescriptors(String storageString) {
+ if (storageString.length() == 0) {
+ return;
+ }
+
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ ValueDisplayDescriptor desc = new ValueDisplayDescriptor();
+ desc.loadFrom(value);
+ mValueDescriptors.add(desc);
+ }
+ }
+
+ /**
+ * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another
+ * list if they are configured to display the {@link EventContainer}
+ *
+ * @param event the event container
+ * @param fullList the list with all the descriptors.
+ * @param outList the list to fill.
+ */
+ @SuppressWarnings("unchecked")
+ private void getDescriptors(EventContainer event,
+ ArrayList<? extends OccurrenceDisplayDescriptor> fullList,
+ ArrayList outList) {
+ for (OccurrenceDisplayDescriptor descriptor : fullList) {
+ try {
+ // first check the event tag.
+ if (descriptor.eventTag == event.mTag) {
+ // now check if we have a filter on a value
+ if (descriptor.filterValueIndex == -1 ||
+ event.testValue(descriptor.filterValueIndex, descriptor.filterValue,
+ descriptor.filterCompareMethod)) {
+ outList.add(descriptor);
+ }
+ }
+ } catch (InvalidTypeException ite) {
+ // if the filter for the descriptor was incorrect, we ignore the descriptor.
+ } catch (ArrayIndexOutOfBoundsException aioobe) {
+ // if the index was wrong (the event content may have changed since we setup the
+ // display), we do nothing but log the error
+ Log.e("Event Log", String.format(
+ "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$
+ descriptor.filterValueIndex, descriptor.eventTag));
+ }
+ }
+ }
+
+ /**
+ * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor}
+ * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event.
+ *
+ * @param event
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ * @return true if the event should be displayed.
+ */
+
+ protected boolean filterEvent(EventContainer event,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+
+ // test the pid first (if needed)
+ if (mPidFiltering && mPidFilterList != null) {
+ boolean found = false;
+ for (int pid : mPidFilterList) {
+ if (pid == event.pid) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found == false) {
+ return false;
+ }
+ }
+
+ // now get the list of matching descriptors
+ getDescriptors(event, mValueDescriptors, valueDescriptors);
+ getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors);
+
+ // and return whether there is at least one match in either list.
+ return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0);
+ }
+
+ /**
+ * Checks all the {@link ValueDisplayDescriptor} for similarity.
+ * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG.
+ * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE
+ *
+ * @return flag as described above
+ */
+ private int checkDescriptors() {
+ if (mValueDescriptors.size() < 2) {
+ return EVENT_CHECK_SAME_VALUE;
+ }
+
+ int tag = -1;
+ int index = -1;
+ for (ValueDisplayDescriptor display : mValueDescriptors) {
+ if (tag == -1) {
+ tag = display.eventTag;
+ index = display.valueIndex;
+ } else {
+ if (tag != display.eventTag) {
+ return EVENT_CHECK_FAILED;
+ } else {
+ if (index != -1) {
+ if (index != display.valueIndex) {
+ index = -1;
+ }
+ }
+ }
+ }
+ }
+
+ if (index == -1) {
+ return EVENT_CHECK_SAME_TAG;
+ }
+
+ return EVENT_CHECK_SAME_VALUE;
+ }
+
+ /**
+ * Resets the time limit on the chart to be infinite.
+ */
+ void resetChartTimeLimit() {
+ mMaximumChartItemAge = -1;
+ }
+
+ /**
+ * Sets the time limit on the charts.
+ *
+ * @param timeLimit the time limit in seconds.
+ */
+ void setChartTimeLimit(long timeLimit) {
+ mMaximumChartItemAge = timeLimit;
+ }
+
+ long getChartTimeLimit() {
+ return mMaximumChartItemAge;
+ }
+
+ /**
+ * m
+ * Resets the histogram width
+ */
+ void resetHistWidth() {
+ mHistWidth = 1;
+ }
+
+ /**
+ * Sets the histogram width
+ *
+ * @param histWidth the width in hours
+ */
+ void setHistWidth(long histWidth) {
+ mHistWidth = histWidth;
+ }
+
+ long getHistWidth() {
+ return mHistWidth;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java
new file mode 100644
index 0000000..b13f3f4
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java
@@ -0,0 +1,961 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+
+class EventDisplayOptions extends Dialog {
+ private static final int DLG_WIDTH = 700;
+ private static final int DLG_HEIGHT = 700;
+
+ private Shell mParent;
+ private Shell mShell;
+
+ private boolean mEditStatus = false;
+ private final ArrayList<EventDisplay> mDisplayList = new ArrayList<EventDisplay>();
+
+ /* LEFT LIST */
+ private List mEventDisplayList;
+ private Button mEventDisplayNewButton;
+ private Button mEventDisplayDeleteButton;
+ private Button mEventDisplayUpButton;
+ private Button mEventDisplayDownButton;
+ private Text mDisplayWidthText;
+ private Text mDisplayHeightText;
+
+ /* WIDGETS ON THE RIGHT */
+ private Text mDisplayNameText;
+ private Combo mDisplayTypeCombo;
+ private Group mChartOptions;
+ private Group mHistOptions;
+ private Button mPidFilterCheckBox;
+ private Text mPidText;
+
+ /** Map with (event-tag, event name) */
+ private Map<Integer, String> mEventTagMap;
+
+ /** Map with (event-tag, array of value info for the event) */
+ private Map<Integer, EventValueDescription[]> mEventDescriptionMap;
+
+ /** list of current pids */
+ private ArrayList<Integer> mPidList;
+
+ private EventLogParser mLogParser;
+
+ private Group mInfoGroup;
+
+ private static class SelectionWidgets {
+ private List mList;
+ private Button mNewButton;
+ private Button mEditButton;
+ private Button mDeleteButton;
+
+ private void setEnabled(boolean enable) {
+ mList.setEnabled(enable);
+ mNewButton.setEnabled(enable);
+ mEditButton.setEnabled(enable);
+ mDeleteButton.setEnabled(enable);
+ }
+ }
+
+ private SelectionWidgets mValueSelection;
+ private SelectionWidgets mOccurrenceSelection;
+
+ /** flag to temporarly disable processing of {@link Text} changes, so that
+ * {@link Text#setText(String)} can be called safely. */
+ private boolean mProcessTextChanges = true;
+ private Text mTimeLimitText;
+ private Text mHistWidthText;
+
+ EventDisplayOptions(Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the
+ * list.
+ * @param logParser
+ * @param displayList
+ * @param eventList
+ * @return true if the list of {@link EventDisplay} objects was updated.
+ */
+ boolean open(EventLogParser logParser, ArrayList<EventDisplay> displayList,
+ ArrayList<EventContainer> eventList) {
+ mLogParser = logParser;
+
+ if (logParser != null) {
+ // we need 2 things from the parser.
+ // the event tag / event name map
+ mEventTagMap = logParser.getTagMap();
+
+ // the event info map
+ mEventDescriptionMap = logParser.getEventInfoMap();
+ }
+
+ // make a copy of the EventDisplay list since we'll use working copies.
+ duplicateEventDisplay(displayList);
+
+ // build a list of pid from the list of events.
+ buildPidList(eventList);
+
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ // Set the dialog size.
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.layout();
+
+ // actually open the dialog
+ mShell.open();
+
+ // event loop until the dialog is closed.
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ return mEditStatus;
+ }
+
+ ArrayList<EventDisplay> getEventDisplays() {
+ return mDisplayList;
+ }
+
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Event Display Configuration");
+
+ mShell.setLayout(new GridLayout(1, true));
+
+ final Composite topPanel = new Composite(mShell, SWT.NONE);
+ topPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ topPanel.setLayout(new GridLayout(2, false));
+
+ // create the tree on the left and the controls on the right.
+ Composite leftPanel = new Composite(topPanel, SWT.NONE);
+ Composite rightPanel = new Composite(topPanel, SWT.NONE);
+
+ createLeftPanel(leftPanel);
+ createRightPanel(rightPanel);
+
+ mShell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ event.doit = true;
+ }
+ });
+
+ Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Composite bottomButtons = new Composite(mShell, SWT.NONE);
+ bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout gl;
+ bottomButtons.setLayout(gl = new GridLayout(2, true));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Button okButton = new Button(bottomButtons, SWT.PUSH);
+ okButton.setText("OK");
+ okButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ Button cancelButton = new Button(bottomButtons, SWT.PUSH);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // cancel the modification flag.
+ mEditStatus = false;
+
+ // and close
+ mShell.close();
+ }
+ });
+
+ enable(false);
+
+ // fill the list with the current display
+ fillEventDisplayList();
+ }
+
+ private void createLeftPanel(Composite leftPanel) {
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ GridLayout gl;
+
+ leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+ leftPanel.setLayout(gl = new GridLayout(1, false));
+ gl.verticalSpacing = 1;
+
+ mEventDisplayList = new List(leftPanel,
+ SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION);
+ mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mEventDisplayList.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleEventDisplaySelection();
+ }
+ });
+
+ Composite bottomControls = new Composite(leftPanel, SWT.NONE);
+ bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ bottomControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing = 0;
+ gl.horizontalSpacing = 0;
+
+ ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+ mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayNewButton.setImage(loader.loadImage("add.png", //$NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayNewButton.setToolTipText("Adds a new event display");
+ mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ createNewEventDisplay();
+ }
+ });
+
+ mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayDeleteButton.setImage(loader.loadImage("delete.png", //$NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display");
+ mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ deleteEventDisplay();
+ }
+ });
+
+ mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayUpButton.setImage(loader.loadImage("up.png", //$NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayUpButton.setToolTipText("Moves the selected event display up");
+ mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get current selection.
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection > 0) {
+ // update the list of EventDisplay.
+ EventDisplay display = mDisplayList.remove(selection);
+ mDisplayList.add(selection - 1, display);
+
+ // update the list widget
+ mEventDisplayList.remove(selection);
+ mEventDisplayList.add(display.getName(), selection - 1);
+
+ // update the selection and reset the ui.
+ mEventDisplayList.select(selection - 1);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+ }
+ });
+
+ mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayDownButton.setImage(loader.loadImage("down.png", //$NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayDownButton.setToolTipText("Moves the selected event display down");
+ mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get current selection.
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) {
+ // update the list of EventDisplay.
+ EventDisplay display = mDisplayList.remove(selection);
+ mDisplayList.add(selection + 1, display);
+
+ // update the list widget
+ mEventDisplayList.remove(selection);
+ mEventDisplayList.add(display.getName(), selection + 1);
+
+ // update the selection and reset the ui.
+ mEventDisplayList.select(selection + 1);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+ }
+ });
+
+ Group sizeGroup = new Group(leftPanel, SWT.NONE);
+ sizeGroup.setText("Display Size:");
+ sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ sizeGroup.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(sizeGroup, SWT.NONE);
+ l.setText("Width:");
+
+ mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+ mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayWidthText.setText(Integer.toString(
+ store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH)));
+ mDisplayWidthText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ String text = mDisplayWidthText.getText().trim();
+ try {
+ store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text));
+ setModified();
+ } catch (NumberFormatException nfe) {
+ // do something?
+ }
+ }
+ });
+
+ l = new Label(sizeGroup, SWT.NONE);
+ l.setText("Height:");
+
+ mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+ mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayHeightText.setText(Integer.toString(
+ store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT)));
+ mDisplayHeightText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ String text = mDisplayHeightText.getText().trim();
+ try {
+ store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text));
+ setModified();
+ } catch (NumberFormatException nfe) {
+ // do something?
+ }
+ }
+ });
+ }
+
+ private void createRightPanel(Composite rightPanel) {
+ rightPanel.setLayout(new GridLayout(1, true));
+ rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mInfoGroup = new Group(rightPanel, SWT.NONE);
+ mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mInfoGroup.setLayout(new GridLayout(2, false));
+
+ Label nameLabel = new Label(mInfoGroup, SWT.LEFT);
+ nameLabel.setText("Name:");
+
+ mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+ mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayNameText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mProcessTextChanges) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ eventDisplay.setName(mDisplayNameText.getText());
+ int index = mEventDisplayList.getSelectionIndex();
+ mEventDisplayList.remove(index);
+ mEventDisplayList.add(eventDisplay.getName(), index);
+ mEventDisplayList.select(index);
+ handleEventDisplaySelection();
+ setModified();
+ }
+ }
+ }
+ });
+
+ Label displayLabel = new Label(mInfoGroup, SWT.LEFT);
+ displayLabel.setText("Type:");
+
+ mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
+ mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_*
+ mDisplayTypeCombo.add("Log All");
+ mDisplayTypeCombo.add("Filtered Log");
+ mDisplayTypeCombo.add("Graph");
+ mDisplayTypeCombo.add("Sync");
+ mDisplayTypeCombo.add("Sync Histogram");
+ mDisplayTypeCombo.add("Sync Performance");
+ mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) {
+ /* Replace the EventDisplay object with a different subclass */
+ setModified();
+ String name = eventDisplay.getName();
+ EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name);
+ setCurrentEventDisplay(newEventDisplay);
+ fillUiWith(newEventDisplay);
+ }
+ }
+ });
+
+ mChartOptions = new Group(mInfoGroup, SWT.NONE);
+ mChartOptions.setText("Chart Options");
+ GridData gd;
+ mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+ mChartOptions.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(mChartOptions, SWT.NONE);
+ l.setText("Time Limit (seconds):");
+
+ mTimeLimitText = new Text(mChartOptions, SWT.BORDER);
+ mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTimeLimitText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ String text = mTimeLimitText.getText().trim();
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ try {
+ if (text.length() == 0) {
+ eventDisplay.resetChartTimeLimit();
+ } else {
+ eventDisplay.setChartTimeLimit(Long.parseLong(text));
+ }
+ } catch (NumberFormatException nfe) {
+ eventDisplay.resetChartTimeLimit();
+ } finally {
+ setModified();
+ }
+ }
+ }
+ });
+
+ mHistOptions = new Group(mInfoGroup, SWT.NONE);
+ mHistOptions.setText("Histogram Options");
+ GridData gdh;
+ mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL));
+ gdh.horizontalSpan = 2;
+ mHistOptions.setLayout(new GridLayout(2, false));
+
+ Label lh = new Label(mHistOptions, SWT.NONE);
+ lh.setText("Histogram width (hours):");
+
+ mHistWidthText = new Text(mHistOptions, SWT.BORDER);
+ mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mHistWidthText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ String text = mHistWidthText.getText().trim();
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ try {
+ if (text.length() == 0) {
+ eventDisplay.resetHistWidth();
+ } else {
+ eventDisplay.setHistWidth(Long.parseLong(text));
+ }
+ } catch (NumberFormatException nfe) {
+ eventDisplay.resetHistWidth();
+ } finally {
+ setModified();
+ }
+ }
+ }
+ });
+
+ mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK);
+ mPidFilterCheckBox.setText("Enable filtering by pid");
+ mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+ mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection());
+ mPidText.setEnabled(mPidFilterCheckBox.getSelection());
+ setModified();
+ }
+ }
+ });
+
+ Label pidLabel = new Label(mInfoGroup, SWT.NONE);
+ pidLabel.setText("Pid Filter:");
+ pidLabel.setToolTipText("Enter all pids, separated by commas");
+
+ mPidText = new Text(mInfoGroup, SWT.BORDER);
+ mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mPidText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mProcessTextChanges) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null && eventDisplay.getPidFiltering()) {
+ String pidText = mPidText.getText().trim();
+ String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$
+
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ for (String pid : pids) {
+ try {
+ list.add(Integer.valueOf(pid));
+ } catch (NumberFormatException nfe) {
+ // just ignore non valid pid
+ }
+ }
+
+ eventDisplay.setPidFilterList(list);
+ setModified();
+ }
+ }
+ }
+ });
+
+ /* ------------------
+ * EVENT VALUE/OCCURRENCE SELECTION
+ * ------------------ */
+ mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class,
+ "Event Value Display");
+ mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class,
+ "Event Occurrence Display");
+ }
+
+ private SelectionWidgets createEventSelection(Composite rightPanel,
+ final Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+ String groupMessage) {
+
+ Group eventSelectionPanel = new Group(rightPanel, SWT.NONE);
+ eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl;
+ eventSelectionPanel.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ eventSelectionPanel.setText(groupMessage);
+
+ final SelectionWidgets widgets = new SelectionWidgets();
+
+ widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL);
+ widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH));
+ widgets.mList.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ widgets.mDeleteButton.setEnabled(true);
+ widgets.mEditButton.setEnabled(true);
+ } else {
+ widgets.mDeleteButton.setEnabled(false);
+ widgets.mEditButton.setEnabled(false);
+ }
+ }
+ });
+
+ Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE);
+ rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+ rightControls.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing = 0;
+ gl.horizontalSpacing = 0;
+
+ widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mNewButton.setText("New...");
+ widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mNewButton.setEnabled(false);
+ widgets.mNewButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ try {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ EventValueSelector dialog = new EventValueSelector(mShell);
+ if (dialog.open(descriptorClass, mLogParser)) {
+ eventDisplay.addDescriptor(dialog.getDescriptor());
+ fillUiWith(eventDisplay);
+ setModified();
+ }
+ }
+ } catch (Exception e1) {
+ e1.printStackTrace();
+ }
+ }
+ });
+
+ widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mEditButton.setText("Edit...");
+ widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mEditButton.setEnabled(false);
+ widgets.mEditButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // get the current descriptor index
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ // get the descriptor itself
+ OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor(
+ descriptorClass, index);
+
+ // open the edit dialog.
+ EventValueSelector dialog = new EventValueSelector(mShell);
+ if (dialog.open(descriptor, mLogParser)) {
+ descriptor.replaceWith(dialog.getDescriptor());
+ eventDisplay.updateValueDescriptorCheck();
+ fillUiWith(eventDisplay);
+
+ // reselect the item since fillUiWith remove the selection.
+ widgets.mList.select(index);
+ widgets.mList.notifyListeners(SWT.Selection, null);
+
+ setModified();
+ }
+ }
+ }
+ }
+ });
+
+ widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mDeleteButton.setText("Delete");
+ widgets.mDeleteButton.setEnabled(false);
+ widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // get the current descriptor index
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ eventDisplay.removeDescriptor(descriptorClass, index);
+ fillUiWith(eventDisplay);
+ setModified();
+ }
+ }
+ }
+ });
+
+ return widgets;
+ }
+
+
+ private void duplicateEventDisplay(ArrayList<EventDisplay> displayList) {
+ for (EventDisplay eventDisplay : displayList) {
+ mDisplayList.add(EventDisplay.clone(eventDisplay));
+ }
+ }
+
+ private void buildPidList(ArrayList<EventContainer> eventList) {
+ mPidList = new ArrayList<Integer>();
+ for (EventContainer event : eventList) {
+ if (mPidList.indexOf(event.pid) == -1) {
+ mPidList.add(event.pid);
+ }
+ }
+ }
+
+ private void setModified() {
+ mEditStatus = true;
+ }
+
+
+ private void enable(boolean status) {
+ mEventDisplayDeleteButton.setEnabled(status);
+
+ // enable up/down
+ int selection = mEventDisplayList.getSelectionIndex();
+ int count = mEventDisplayList.getItemCount();
+ mEventDisplayUpButton.setEnabled(status && selection > 0);
+ mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1);
+
+ mDisplayNameText.setEnabled(status);
+ mDisplayTypeCombo.setEnabled(status);
+ mPidFilterCheckBox.setEnabled(status);
+
+ mValueSelection.setEnabled(status);
+ mOccurrenceSelection.setEnabled(status);
+ mValueSelection.mNewButton.setEnabled(status);
+ mOccurrenceSelection.mNewButton.setEnabled(status);
+ if (status == false) {
+ mPidText.setEnabled(false);
+ }
+ }
+
+ private void fillEventDisplayList() {
+ for (EventDisplay eventDisplay : mDisplayList) {
+ mEventDisplayList.add(eventDisplay.getName());
+ }
+ }
+
+ private void createNewEventDisplay() {
+ int count = mDisplayList.size();
+
+ String name = String.format("display %1$d", count + 1);
+
+ EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name);
+
+ mDisplayList.add(eventDisplay);
+ mEventDisplayList.add(name);
+
+ mEventDisplayList.select(count);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+
+ private void deleteEventDisplay() {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ mDisplayList.remove(selection);
+ mEventDisplayList.remove(selection);
+ if (mDisplayList.size() < selection) {
+ selection--;
+ }
+ mEventDisplayList.select(selection);
+ handleEventDisplaySelection();
+
+ setModified();
+ }
+ }
+
+ private EventDisplay getCurrentEventDisplay() {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ return mDisplayList.get(selection);
+ }
+
+ return null;
+ }
+
+ private void setCurrentEventDisplay(EventDisplay eventDisplay) {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ mDisplayList.set(selection, eventDisplay);
+ }
+ }
+
+ private void handleEventDisplaySelection() {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // enable the UI
+ enable(true);
+
+ // and fill it
+ fillUiWith(eventDisplay);
+ } else {
+ // disable the UI
+ enable(false);
+
+ // and empty it.
+ emptyUi();
+ }
+ }
+
+ private void emptyUi() {
+ mDisplayNameText.setText("");
+ mDisplayTypeCombo.clearSelection();
+ mValueSelection.mList.removeAll();
+ mOccurrenceSelection.mList.removeAll();
+ }
+
+ private void fillUiWith(EventDisplay eventDisplay) {
+ mProcessTextChanges = false;
+
+ mDisplayNameText.setText(eventDisplay.getName());
+ int displayMode = eventDisplay.getDisplayType();
+ mDisplayTypeCombo.select(displayMode);
+ if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) {
+ GridData gd = (GridData) mChartOptions.getLayoutData();
+ gd.exclude = false;
+ mChartOptions.setVisible(!gd.exclude);
+ long limit = eventDisplay.getChartTimeLimit();
+ if (limit != -1) {
+ mTimeLimitText.setText(Long.toString(limit));
+ } else {
+ mTimeLimitText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ GridData gd = (GridData) mChartOptions.getLayoutData();
+ gd.exclude = true;
+ mChartOptions.setVisible(!gd.exclude);
+ mTimeLimitText.setText(""); //$NON-NLS-1$
+ }
+
+ if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) {
+ GridData gd = (GridData) mHistOptions.getLayoutData();
+ gd.exclude = false;
+ mHistOptions.setVisible(!gd.exclude);
+ long limit = eventDisplay.getHistWidth();
+ if (limit != -1) {
+ mHistWidthText.setText(Long.toString(limit));
+ } else {
+ mHistWidthText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ GridData gd = (GridData) mHistOptions.getLayoutData();
+ gd.exclude = true;
+ mHistOptions.setVisible(!gd.exclude);
+ mHistWidthText.setText(""); //$NON-NLS-1$
+ }
+ mInfoGroup.layout(true);
+ mShell.layout(true);
+ mShell.pack();
+
+ if (eventDisplay.getPidFiltering()) {
+ mPidFilterCheckBox.setSelection(true);
+ mPidText.setEnabled(true);
+
+ // build the pid list.
+ ArrayList<Integer> list = eventDisplay.getPidFilterList();
+ if (list != null) {
+ StringBuilder sb = new StringBuilder();
+ int count = list.size();
+ for (int i = 0 ; i < count ; i++) {
+ sb.append(list.get(i));
+ if (i < count - 1) {
+ sb.append(", ");//$NON-NLS-1$
+ }
+ }
+ mPidText.setText(sb.toString());
+ } else {
+ mPidText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ mPidFilterCheckBox.setSelection(false);
+ mPidText.setEnabled(false);
+ mPidText.setText(""); //$NON-NLS-1$
+ }
+
+ mProcessTextChanges = true;
+
+ mValueSelection.mList.removeAll();
+ mOccurrenceSelection.mList.removeAll();
+
+ if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG ||
+ eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) {
+ mOccurrenceSelection.setEnabled(true);
+ mValueSelection.setEnabled(true);
+
+ Iterator<ValueDisplayDescriptor> valueIterator = eventDisplay.getValueDescriptors();
+
+ while (valueIterator.hasNext()) {
+ ValueDisplayDescriptor descriptor = valueIterator.next();
+ mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s",
+ mEventTagMap.get(descriptor.eventTag), descriptor.valueName,
+ getSeriesLabelDescription(descriptor), getFilterDescription(descriptor)));
+ }
+
+ Iterator<OccurrenceDisplayDescriptor> occurrenceIterator =
+ eventDisplay.getOccurrenceDescriptors();
+
+ while (occurrenceIterator.hasNext()) {
+ OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next();
+
+ mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s",
+ mEventTagMap.get(descriptor.eventTag),
+ getSeriesLabelDescription(descriptor),
+ getFilterDescription(descriptor)));
+ }
+
+ mValueSelection.mList.notifyListeners(SWT.Selection, null);
+ mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null);
+ } else {
+ mOccurrenceSelection.setEnabled(false);
+ mValueSelection.setEnabled(false);
+ }
+
+ }
+
+ /**
+ * Returns a String describing what is used as the series label
+ * @param descriptor the descriptor of the display.
+ */
+ private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor.seriesValueIndex != -1) {
+ if (descriptor.includePid) {
+ return String.format("%1$s + pid",
+ mEventDescriptionMap.get(
+ descriptor.eventTag)[descriptor.seriesValueIndex].getName());
+ } else {
+ return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex]
+ .getName();
+ }
+ }
+ return "pid";
+ }
+
+ private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor.filterValueIndex != -1) {
+ return String.format(" [%1$s %2$s %3$s]",
+ mEventDescriptionMap.get(
+ descriptor.eventTag)[descriptor.filterValueIndex].getName(),
+ descriptor.filterCompareMethod.testString(),
+ descriptor.filterValue != null ?
+ descriptor.filterValue.toString() : "?"); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java
new file mode 100644
index 0000000..011bcf1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+/**
+ * Imports a textual event log. Gets tags from build path.
+ */
+public class EventLogImporter {
+
+ private String[] mTags;
+ private String[] mLog;
+
+ public EventLogImporter(String filePath) throws FileNotFoundException {
+ String top = System.getenv("ANDROID_BUILD_TOP");
+ if (top == null) {
+ throw new FileNotFoundException();
+ }
+ final String tagFile = top + "/system/core/logcat/event-log-tags";
+ BufferedReader tagReader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(tagFile)));
+ BufferedReader eventReader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(filePath)));
+ try {
+ readTags(tagReader);
+ readLog(eventReader);
+ } catch (IOException e) {
+ } finally {
+ if (tagReader != null) {
+ try {
+ tagReader.close();
+ } catch (IOException ignore) {
+ }
+ }
+ if (eventReader != null) {
+ try {
+ eventReader.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+
+ public String[] getTags() {
+ return mTags;
+ }
+
+ public String[] getLog() {
+ return mLog;
+ }
+
+ private void readTags(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ content.add(line);
+ }
+ mTags = content.toArray(new String[content.size()]);
+ }
+
+ private void readLog(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ content.add(line);
+ }
+
+ mLog = content.toArray(new String[content.size()]);
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java
new file mode 100644
index 0000000..937ee40
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java
@@ -0,0 +1,938 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.LogReceiver;
+import com.android.ddmlib.log.LogReceiver.ILogListener;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.annotation.WorkerThread;
+import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Event log viewer
+ */
+public class EventLogPanel extends TablePanel implements ILogListener,
+ ILogColumnListener {
+
+ private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$
+
+ private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$
+ private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$
+
+ static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$
+ static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$
+
+ private final static int DEFAULT_DISPLAY_WIDTH = 500;
+ private final static int DEFAULT_DISPLAY_HEIGHT = 400;
+
+ private IDevice mCurrentLoggedDevice;
+ private String mCurrentLogFile;
+ private LogReceiver mCurrentLogReceiver;
+ private EventLogParser mCurrentEventLogParser;
+
+ private Object mLock = new Object();
+
+ /** list of all the events. */
+ private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>();
+
+ /** list of all the new events, that have yet to be displayed by the ui */
+ private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>();
+ /** indicates a pending ui thread display */
+ private boolean mPendingDisplay = false;
+
+ /** list of all the custom event displays */
+ private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>();
+
+ private final NumberFormat mFormatter = NumberFormat.getInstance();
+ private Composite mParent;
+ private ScrolledComposite mBottomParentPanel;
+ private Composite mBottomPanel;
+ private ICommonAction mOptionsAction;
+ private ICommonAction mClearAction;
+ private ICommonAction mSaveAction;
+ private ICommonAction mLoadAction;
+ private ICommonAction mImportAction;
+
+ /** file containing the current log raw data. */
+ private File mTempFile = null;
+
+ public EventLogPanel() {
+ super();
+ mFormatter.setGroupingUsed(true);
+ }
+
+ /**
+ * Sets the external actions.
+ * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code
+ * when triggered by using {@link ICommonAction#setRunnable(Runnable)}.
+ * <p/>It will also make sure they are enabled only when possible.
+ * @param optionsAction
+ * @param clearAction
+ * @param saveAction
+ * @param loadAction
+ * @param importAction
+ */
+ public void setActions(ICommonAction optionsAction, ICommonAction clearAction,
+ ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) {
+ mOptionsAction = optionsAction;
+ mOptionsAction.setRunnable(new Runnable() {
+ @Override
+ public void run() {
+ openOptionPanel();
+ }
+ });
+
+ mClearAction = clearAction;
+ mClearAction.setRunnable(new Runnable() {
+ @Override
+ public void run() {
+ clearLog();
+ }
+ });
+
+ mSaveAction = saveAction;
+ mSaveAction.setRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Save Event Log");
+ fileDialog.setFileName("event.log");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ saveLog(fileName);
+ }
+ } catch (IOException e1) {
+ }
+ }
+ });
+
+ mLoadAction = loadAction;
+ mLoadAction.setRunnable(new Runnable() {
+ @Override
+ public void run() {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load Event Log");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ loadLog(fileName);
+ }
+ }
+ });
+
+ mImportAction = importAction;
+ mImportAction.setRunnable(new Runnable() {
+ @Override
+ public void run() {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Import Bug Report");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ importBugReport(fileName);
+ }
+ }
+ });
+
+ mOptionsAction.setEnabled(false);
+ mClearAction.setEnabled(false);
+ mSaveAction.setEnabled(false);
+ }
+
+ /**
+ * Opens the option panel.
+ * </p>
+ * <b>This must be called from the UI thread</b>
+ */
+ @UiThread
+ public void openOptionPanel() {
+ try {
+ EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell());
+ if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) {
+ synchronized (mLock) {
+ // get the new EventDisplay list
+ mEventDisplays.clear();
+ mEventDisplays.addAll(dialog.getEventDisplays());
+
+ // since the list of EventDisplay changed, we store it.
+ saveEventDisplays();
+
+ rebuildUi();
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("EventLog", e); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Clears the log.
+ * <p/>
+ * <b>This must be called from the UI thread</b>
+ */
+ public void clearLog() {
+ try {
+ synchronized (mLock) {
+ mEvents.clear();
+ mNewEvents.clear();
+ mPendingDisplay = false;
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resetUI();
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("EventLog", e); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Saves the content of the event log into a file. The log is saved in the same
+ * binary format than on the device.
+ * @param filePath
+ * @throws IOException
+ */
+ public void saveLog(String filePath) throws IOException {
+ if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) {
+ File destFile = new File(filePath);
+ destFile.createNewFile();
+ FileInputStream fis = new FileInputStream(mTempFile);
+ FileOutputStream fos = new FileOutputStream(destFile);
+ byte[] buffer = new byte[1024];
+
+ int count;
+
+ while ((count = fis.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+
+ fos.close();
+ fis.close();
+
+ // now we save the tag file
+ filePath = filePath + TAG_FILE_EXT;
+ mCurrentEventLogParser.saveTags(filePath);
+ }
+ }
+
+ /**
+ * Loads a binary event log (if has associated .tag file) or
+ * otherwise loads a textual event log.
+ * @param filePath Event log path (and base of potential tag file)
+ */
+ public void loadLog(String filePath) {
+ if ((new File(filePath + TAG_FILE_EXT)).exists()) {
+ startEventLogFromFiles(filePath);
+ } else {
+ try {
+ EventLogImporter importer = new EventLogImporter(filePath);
+ String[] tags = importer.getTags();
+ String[] log = importer.getLog();
+ startEventLogFromContent(tags, log);
+ } catch (FileNotFoundException e) {
+ // If this fails, display the error message from startEventLogFromFiles,
+ // and pretend we never tried EventLogImporter
+ Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog",
+ String.format("Failure to read %1$s", filePath + TAG_FILE_EXT));
+ }
+
+ }
+ }
+
+ public void importBugReport(String filePath) {
+ try {
+ BugReportImporter importer = new BugReportImporter(filePath);
+
+ String[] tags = importer.getTags();
+ String[] log = importer.getLog();
+
+ startEventLogFromContent(tags, log);
+
+ } catch (FileNotFoundException e) {
+ Log.logAndDisplay(LogLevel.ERROR, "Import",
+ "Unable to import bug report: " + e.getMessage());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected()
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected()
+ */
+ @Override
+ public void deviceSelected() {
+ startEventLog(getCurrentDevice());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int)
+ */
+ @Override
+ public void clientChanged(Client client, int changeMask) {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+ mParent.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ synchronized (mLock) {
+ if (mCurrentLogReceiver != null) {
+ mCurrentLogReceiver.cancel();
+ mCurrentLogReceiver = null;
+ mCurrentEventLogParser = null;
+ mCurrentLoggedDevice = null;
+ mEventDisplays.clear();
+ mEvents.clear();
+ }
+ }
+ }
+ });
+
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // init some store stuff
+ store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH);
+ store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT);
+
+ mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL);
+ mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mBottomParentPanel.setExpandHorizontal(true);
+ mBottomParentPanel.setExpandVertical(true);
+
+ mBottomParentPanel.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ if (mBottomPanel != null) {
+ Rectangle r = mBottomParentPanel.getClientArea();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+ SWT.DEFAULT));
+ }
+ }
+ });
+
+ prepareDisplayUi();
+
+ // load the EventDisplay from storage.
+ loadEventDisplays();
+
+ // create the ui
+ createDisplayUi();
+
+ return mBottomParentPanel;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#postCreation()
+ */
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#setFocus()
+ */
+ @Override
+ public void setFocus() {
+ mBottomParentPanel.setFocus();
+ }
+
+ /**
+ * Starts a new logcat and set mCurrentLogCat as the current receiver.
+ * @param device the device to connect logcat to.
+ */
+ private void startEventLog(final IDevice device) {
+ if (device == mCurrentLoggedDevice) {
+ return;
+ }
+
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ if (device != null) {
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ while (device.isOnline() == false &&
+ mCurrentLogReceiver != null &&
+ mCurrentLogReceiver.isCancelled() == false) {
+ try {
+ sleep(2000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) {
+ // logcat was stopped/cancelled before the device became ready.
+ return;
+ }
+
+ try {
+ mCurrentLoggedDevice = device;
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ mCurrentEventLogParser.init(device);
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ // prepare the temp file that will contain the raw data
+ mTempFile = File.createTempFile("android-event-", ".log");
+
+ device.runEventLogService(mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+ }
+
+ private void startEventLogFromFiles(final String fileName) {
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ mSaveAction.setEnabled(false);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ mCurrentLogFile = fileName;
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) {
+ mCurrentEventLogParser = null;
+ Log.logAndDisplay(LogLevel.ERROR, "EventLog",
+ String.format("Failure to read %1$s", fileName + TAG_FILE_EXT));
+ return;
+ }
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ runLocalEventLogService(fileName, mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+
+ private void startEventLogFromContent(final String[] tags, final String[] log) {
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ mSaveAction.setEnabled(false);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ if (mCurrentEventLogParser.init(tags) == false) {
+ mCurrentEventLogParser = null;
+ return;
+ }
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ runLocalEventLogService(log, mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+
+
+ public void stopEventLog(boolean inUiThread) {
+ if (mCurrentLogReceiver != null) {
+ mCurrentLogReceiver.cancel();
+
+ // when the thread finishes, no one will reference that object
+ // and it'll be destroyed
+ synchronized (mLock) {
+ mCurrentLogReceiver = null;
+ mCurrentEventLogParser = null;
+
+ mCurrentLoggedDevice = null;
+ mEvents.clear();
+ mNewEvents.clear();
+ mPendingDisplay = false;
+ }
+
+ resetUI(inUiThread);
+ }
+
+ if (mTempFile != null) {
+ mTempFile.delete();
+ mTempFile = null;
+ }
+ }
+
+ private void resetUI(boolean inUiThread) {
+ mEvents.clear();
+
+ // the ui is static we just empty it.
+ if (inUiThread) {
+ resetUiFromUiThread();
+ } else {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ resetUiFromUiThread();
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed, we're quitting. Do nothing.
+ }
+ }
+ }
+
+ private void resetUiFromUiThread() {
+ synchronized (mLock) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resetUI();
+ }
+ }
+ mOptionsAction.setEnabled(false);
+ mClearAction.setEnabled(false);
+ mSaveAction.setEnabled(false);
+ }
+
+ private void prepareDisplayUi() {
+ mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE);
+ mBottomParentPanel.setContent(mBottomPanel);
+ }
+
+ private void createDisplayUi() {
+ RowLayout rowLayout = new RowLayout();
+ rowLayout.wrap = true;
+ rowLayout.pack = false;
+ rowLayout.justify = true;
+ rowLayout.fill = true;
+ rowLayout.type = SWT.HORIZONTAL;
+ mBottomPanel.setLayout(rowLayout);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+ int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH);
+ int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT);
+
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this);
+ if (c != null) {
+ RowData rd = new RowData();
+ rd.height = displayHeight;
+ rd.width = displayWidth;
+ c.setLayoutData(rd);
+ }
+
+ Table table = eventDisplay.getTable();
+ if (table != null) {
+ addTableToFocusListener(table);
+ }
+ }
+
+ mBottomPanel.layout();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+ mBottomParentPanel.layout();
+ }
+
+ /**
+ * Rebuild the display ui.
+ */
+ @UiThread
+ private void rebuildUi() {
+ synchronized (mLock) {
+ // we need to rebuild the ui. First we get rid of it.
+ mBottomPanel.dispose();
+ mBottomPanel = null;
+
+ prepareDisplayUi();
+ createDisplayUi();
+
+ // and fill it
+
+ boolean start_event = false;
+ synchronized (mNewEvents) {
+ mNewEvents.addAll(0, mEvents);
+
+ if (mPendingDisplay == false) {
+ mPendingDisplay = true;
+ start_event = true;
+ }
+ }
+
+ if (start_event) {
+ scheduleUIEventHandler();
+ }
+
+ Rectangle r = mBottomParentPanel.getClientArea();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+ SWT.DEFAULT));
+ }
+ }
+
+
+ /**
+ * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it.
+ * @param entry The new log entry
+ * @see LogReceiver.ILogListener#newEntry(LogEntry)
+ */
+ @Override
+ @WorkerThread
+ public void newEntry(LogEntry entry) {
+ synchronized (mLock) {
+ if (mCurrentEventLogParser != null) {
+ EventContainer event = mCurrentEventLogParser.parse(entry);
+ if (event != null) {
+ handleNewEvent(event);
+ }
+ }
+ }
+ }
+
+ @WorkerThread
+ private void handleNewEvent(EventContainer event) {
+ // add the event to the generic list
+ mEvents.add(event);
+
+ // add to the list of events that needs to be displayed, and trigger a
+ // new display if needed.
+ boolean start_event = false;
+ synchronized (mNewEvents) {
+ mNewEvents.add(event);
+
+ if (mPendingDisplay == false) {
+ mPendingDisplay = true;
+ start_event = true;
+ }
+ }
+
+ if (start_event == false) {
+ // we're done
+ return;
+ }
+
+ scheduleUIEventHandler();
+ }
+
+ /**
+ * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}.
+ */
+ private void scheduleUIEventHandler() {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ if (mCurrentEventLogParser != null) {
+ displayNewEvents();
+ }
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // if the ui is disposed, do nothing
+ }
+ }
+
+ /**
+ * Processes raw data coming from the log service.
+ * @see LogReceiver.ILogListener#newData(byte[], int, int)
+ */
+ @Override
+ public void newData(byte[] data, int offset, int length) {
+ if (mTempFile != null) {
+ try {
+ FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */);
+ fos.write(data, offset, length);
+ fos.close();
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ @UiThread
+ private void displayNewEvents() {
+ // never display more than 1,000 events in this loop. We can't do too much in the UI thread.
+ int count = 0;
+
+ // prepare the displays
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.startMultiEventDisplay();
+ }
+
+ // display the new events
+ EventContainer event = null;
+ boolean need_to_reloop = false;
+ do {
+ // get the next event to display.
+ synchronized (mNewEvents) {
+ if (mNewEvents.size() > 0) {
+ if (count > 200) {
+ // there are still events to be displayed, but we don't want to hog the
+ // UI thread for too long, so we stop this runnable, but launch a new
+ // one to keep going.
+ need_to_reloop = true;
+ event = null;
+ } else {
+ event = mNewEvents.remove(0);
+ count++;
+ }
+ } else {
+ // we're done.
+ event = null;
+ mPendingDisplay = false;
+ }
+ }
+
+ if (event != null) {
+ // notify the event display
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.newEvent(event, mCurrentEventLogParser);
+ }
+ }
+ } while (event != null);
+
+ // we're done displaying events.
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.endMultiEventDisplay();
+ }
+
+ // if needed, ask the UI thread to re-run this method.
+ if (need_to_reloop) {
+ scheduleUIEventHandler();
+ }
+ }
+
+ /**
+ * Loads the {@link EventDisplay}s from the preference store.
+ */
+ private void loadEventDisplays() {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+ String storage = store.getString(PREFS_EVENT_DISPLAY);
+
+ if (storage.length() > 0) {
+ String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ EventDisplay eventDisplay = EventDisplay.load(value);
+ if (eventDisplay != null) {
+ mEventDisplays.add(eventDisplay);
+ }
+ }
+ }
+ }
+
+ /**
+ * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store.
+ */
+ private void saveEventDisplays() {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ boolean first = true;
+ StringBuilder sb = new StringBuilder();
+
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ String storage = eventDisplay.getStorageString();
+ if (storage != null) {
+ if (first == false) {
+ sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+
+ sb.append(storage);
+ }
+ }
+
+ store.setValue(PREFS_EVENT_DISPLAY, sb.toString());
+ }
+
+ /**
+ * Updates the {@link EventDisplay} with the new {@link EventLogParser}.
+ * <p/>
+ * This will run asynchronously in the UI thread.
+ */
+ @WorkerThread
+ private void updateEventDisplays() {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.setNewLogParser(mCurrentEventLogParser);
+ }
+
+ mOptionsAction.setEnabled(true);
+ mClearAction.setEnabled(true);
+ if (mCurrentLogFile == null) {
+ mSaveAction.setEnabled(true);
+ } else {
+ mSaveAction.setEnabled(false);
+ }
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed: do nothing.
+ }
+ }
+
+ @Override
+ @UiThread
+ public void columnResized(int index, TableColumn sourceColumn) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resizeColumn(index, sourceColumn);
+ }
+ }
+
+ /**
+ * Runs an event log service out of a local file.
+ * @param fileName the full file name of the local file containing the event log.
+ * @param logReceiver the receiver that will handle the log
+ * @throws IOException
+ */
+ @WorkerThread
+ private void runLocalEventLogService(String fileName, LogReceiver logReceiver)
+ throws IOException {
+ byte[] buffer = new byte[256];
+
+ FileInputStream fis = new FileInputStream(fileName);
+ try {
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ logReceiver.parseNewData(buffer, 0, count);
+ }
+ } finally {
+ fis.close();
+ }
+ }
+
+ @WorkerThread
+ private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) {
+ synchronized (mLock) {
+ for (String line : log) {
+ EventContainer event = mCurrentEventLogParser.parse(line);
+ if (event != null) {
+ handleNewEvent(event);
+ }
+ }
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java
new file mode 100644
index 0000000..e7c5196
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Set;
+
+final class EventValueSelector extends Dialog {
+ private static final int DLG_WIDTH = 400;
+ private static final int DLG_HEIGHT = 300;
+
+ private Shell mParent;
+ private Shell mShell;
+ private boolean mEditStatus;
+ private Combo mEventCombo;
+ private Combo mValueCombo;
+ private Combo mSeriesCombo;
+ private Button mDisplayPidCheckBox;
+ private Combo mFilterCombo;
+ private Combo mFilterMethodCombo;
+ private Text mFilterValue;
+ private Button mOkButton;
+
+ private EventLogParser mLogParser;
+ private OccurrenceDisplayDescriptor mDescriptor;
+
+ /** list of event integer in the order of the combo. */
+ private Integer[] mEventTags;
+
+ /** list of indices in the {@link EventValueDescription} array of the current event
+ * that are of type string. This lets us get back the {@link EventValueDescription} from the
+ * index in the Series {@link Combo}.
+ */
+ private final ArrayList<Integer> mSeriesIndices = new ArrayList<Integer>();
+
+ public EventValueSelector(Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Opens the display option dialog to edit a new descriptor.
+ * @param decriptorClass the class of the object to instantiate. Must extend
+ * {@link OccurrenceDisplayDescriptor}
+ * @param logParser
+ * @return true if the object is to be created, false if the creation was canceled.
+ */
+ boolean open(Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+ EventLogParser logParser) {
+ try {
+ OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance();
+ setModified();
+ return open(descriptor, logParser);
+ } catch (InstantiationException e) {
+ return false;
+ } catch (IllegalAccessException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or
+ * a {@link ValueDisplayDescriptor} object.
+ * @param descriptor The descriptor to edit.
+ * @return true if the object was modified.
+ */
+ boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) {
+ // make a copy of the descriptor as we'll use a working copy.
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor);
+ } else if (descriptor instanceof OccurrenceDisplayDescriptor) {
+ mDescriptor = new OccurrenceDisplayDescriptor(descriptor);
+ } else {
+ return false;
+ }
+
+ mLogParser = logParser;
+
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ loadValueDescriptor();
+
+ checkValidity();
+
+ // Set the dialog size.
+ try {
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ mShell.layout();
+
+ // actually open the dialog
+ mShell.open();
+
+ // event loop until the dialog is closed.
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ return mEditStatus;
+ }
+
+ OccurrenceDisplayDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ private void createUI() {
+ GridData gd;
+
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Event Display Configuration");
+
+ mShell.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(mShell, SWT.NONE);
+ l.setText("Event:");
+
+ mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // the event tag / event name map
+ Map<Integer, String> eventTagMap = mLogParser.getTagMap();
+ Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap();
+ Set<Integer> keys = eventTagMap.keySet();
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ for (Integer i : keys) {
+ if (eventInfoMap.get(i) != null) {
+ String eventName = eventTagMap.get(i);
+ mEventCombo.add(eventName);
+
+ list.add(i);
+ }
+ }
+ mEventTags = list.toArray(new Integer[list.size()]);
+
+ mEventCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleEventComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Value:");
+
+ mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mValueCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleValueComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Series Name:");
+
+ mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mSeriesCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleSeriesComboSelection();
+ setModified();
+ }
+ });
+
+ // empty comp
+ new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+
+ mDisplayPidCheckBox = new Button(mShell, SWT.CHECK);
+ mDisplayPidCheckBox.setText("Also Show pid");
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDescriptor.includePid = mDisplayPidCheckBox.getSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter By:");
+
+ mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleFilterComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter Method:");
+
+ mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (CompareMethod method : CompareMethod.values()) {
+ mFilterMethodCombo.add(method.toString());
+ }
+ mFilterMethodCombo.select(0);
+ mFilterMethodCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleFilterMethodComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter Value:");
+
+ mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE);
+ mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterValue.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mDescriptor.filterValueIndex != -1) {
+ // get the current selection in the event combo
+ int index = mEventCombo.getSelectionIndex();
+
+ if (index != -1) {
+ // match it to an event
+ int eventTag = mEventTags[index];
+ mDescriptor.eventTag = eventTag;
+
+ // get the EventValueDescription for this tag
+ EventValueDescription valueDesc = mLogParser.getEventInfoMap()
+ .get(eventTag)[mDescriptor.filterValueIndex];
+
+ // let the EventValueDescription convert the String value into an object
+ // of the proper type.
+ mDescriptor.filterValue = valueDesc.getObjectFromString(
+ mFilterValue.getText().trim());
+ setModified();
+ }
+ }
+ }
+ });
+
+ // add a separator spanning the 2 columns
+
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+
+ // add a composite to hold the ok/cancel button, no matter what the columns size are.
+ Composite buttonComp = new Composite(mShell, SWT.NONE);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ buttonComp.setLayoutData(gd);
+ GridLayout gl;
+ buttonComp.setLayout(gl = new GridLayout(6, true));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Composite padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mOkButton = new Button(buttonComp, SWT.PUSH);
+ mOkButton.setText("OK");
+ mOkButton.setLayoutData(new GridData(GridData.CENTER));
+ mOkButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Button cancelButton = new Button(buttonComp, SWT.PUSH);
+ cancelButton.setText("Cancel");
+ cancelButton.setLayoutData(new GridData(GridData.CENTER));
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // cancel the edit
+ mEditStatus = false;
+ mShell.close();
+ }
+ });
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ event.doit = true;
+ }
+ });
+ }
+
+ private void setModified() {
+ mEditStatus = true;
+ }
+
+ private void handleEventComboSelection() {
+ // get the current selection in the event combo
+ int index = mEventCombo.getSelectionIndex();
+
+ if (index != -1) {
+ // match it to an event
+ int eventTag = mEventTags[index];
+ mDescriptor.eventTag = eventTag;
+
+ // get the EventValueDescription for this tag
+ EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+ // fill the combo for the values
+ mValueCombo.removeAll();
+ if (values != null) {
+ if (mDescriptor instanceof ValueDisplayDescriptor) {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+ mValueCombo.setEnabled(true);
+ for (EventValueDescription value : values) {
+ mValueCombo.add(value.toString());
+ }
+
+ if (valueDescriptor.valueIndex != -1) {
+ mValueCombo.select(valueDescriptor.valueIndex);
+ } else {
+ mValueCombo.clearSelection();
+ }
+ } else {
+ mValueCombo.setEnabled(false);
+ }
+
+ // fill the axis combo
+ mSeriesCombo.removeAll();
+ mSeriesCombo.setEnabled(false);
+ mSeriesIndices.clear();
+ int axisIndex = 0;
+ int selectionIndex = -1;
+ for (EventValueDescription value : values) {
+ if (value.getEventValueType() == EventValueType.STRING) {
+ mSeriesCombo.add(value.getName());
+ mSeriesCombo.setEnabled(true);
+ mSeriesIndices.add(axisIndex);
+
+ if (mDescriptor.seriesValueIndex != -1 &&
+ mDescriptor.seriesValueIndex == axisIndex) {
+ selectionIndex = axisIndex;
+ }
+ }
+ axisIndex++;
+ }
+
+ if (mSeriesCombo.isEnabled()) {
+ mSeriesCombo.add("default (pid)", 0 /* index */);
+ mSeriesIndices.add(0 /* index */, -1 /* value */);
+
+ // +1 because we added another item at index 0
+ mSeriesCombo.select(selectionIndex + 1);
+
+ if (selectionIndex >= 0) {
+ mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+ mDisplayPidCheckBox.setEnabled(true);
+ } else {
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.setSelection(false);
+ }
+ } else {
+ mDisplayPidCheckBox.setSelection(false);
+ mDisplayPidCheckBox.setEnabled(false);
+ }
+
+ // fill the filter combo
+ mFilterCombo.setEnabled(true);
+ mFilterCombo.removeAll();
+ mFilterCombo.add("(no filter)");
+ for (EventValueDescription value : values) {
+ mFilterCombo.add(value.toString());
+ }
+
+ // select the current filter
+ mFilterCombo.select(mDescriptor.filterValueIndex + 1);
+ mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod));
+
+ // fill the current filter value
+ if (mDescriptor.filterValueIndex != -1) {
+ EventValueDescription valueInfo = values[mDescriptor.filterValueIndex];
+ if (valueInfo.checkForType(mDescriptor.filterValue)) {
+ mFilterValue.setText(mDescriptor.filterValue.toString());
+ } else {
+ mFilterValue.setText("");
+ }
+ } else {
+ mFilterValue.setText("");
+ }
+ } else {
+ disableSubCombos();
+ }
+ } else {
+ disableSubCombos();
+ }
+
+ checkValidity();
+ }
+
+ /**
+ *
+ */
+ private void disableSubCombos() {
+ mValueCombo.removeAll();
+ mValueCombo.clearSelection();
+ mValueCombo.setEnabled(false);
+
+ mSeriesCombo.removeAll();
+ mSeriesCombo.clearSelection();
+ mSeriesCombo.setEnabled(false);
+
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.setSelection(false);
+
+ mFilterCombo.removeAll();
+ mFilterCombo.clearSelection();
+ mFilterCombo.setEnabled(false);
+
+ mFilterValue.setEnabled(false);
+ mFilterValue.setText("");
+ mFilterMethodCombo.setEnabled(false);
+ }
+
+ private void handleValueComboSelection() {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+ // get the current selection in the value combo
+ int index = mValueCombo.getSelectionIndex();
+ valueDescriptor.valueIndex = index;
+
+ // for now set the built-in name
+
+ // get the current selection in the event combo
+ int eventIndex = mEventCombo.getSelectionIndex();
+
+ // match it to an event
+ int eventTag = mEventTags[eventIndex];
+
+ // get the EventValueDescription for this tag
+ EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+ valueDescriptor.valueName = values[index].getName();
+
+ checkValidity();
+ }
+
+ private void handleSeriesComboSelection() {
+ // get the current selection in the axis combo
+ int index = mSeriesCombo.getSelectionIndex();
+
+ // get the actual value index from the list.
+ int valueIndex = mSeriesIndices.get(index);
+
+ mDescriptor.seriesValueIndex = valueIndex;
+
+ if (index > 0) {
+ mDisplayPidCheckBox.setEnabled(true);
+ mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+ } else {
+ mDisplayPidCheckBox.setSelection(false);
+ mDisplayPidCheckBox.setEnabled(false);
+ }
+ }
+
+ private void handleFilterComboSelection() {
+ // get the current selection in the axis combo
+ int index = mFilterCombo.getSelectionIndex();
+
+ // decrement index by 1 since the item 0 means
+ // no filter (index = -1), and the rest is offset by 1
+ index--;
+
+ mDescriptor.filterValueIndex = index;
+
+ if (index != -1) {
+ mFilterValue.setEnabled(true);
+ mFilterMethodCombo.setEnabled(true);
+ if (mDescriptor.filterValue instanceof String) {
+ mFilterValue.setText((String)mDescriptor.filterValue);
+ }
+ } else {
+ mFilterValue.setText("");
+ mFilterValue.setEnabled(false);
+ mFilterMethodCombo.setEnabled(false);
+ }
+ }
+
+ private void handleFilterMethodComboSelection() {
+ // get the current selection in the axis combo
+ int index = mFilterMethodCombo.getSelectionIndex();
+ CompareMethod method = CompareMethod.values()[index];
+
+ mDescriptor.filterCompareMethod = method;
+ }
+
+ /**
+ * Returns the index of the filter method
+ * @param filterCompareMethod the {@link CompareMethod} enum.
+ */
+ private int getFilterMethodIndex(CompareMethod filterCompareMethod) {
+ CompareMethod[] values = CompareMethod.values();
+ for (int i = 0 ; i < values.length ; i++) {
+ if (values[i] == filterCompareMethod) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+
+ private void loadValueDescriptor() {
+ // get the index from the eventTag.
+ int eventIndex = 0;
+ int comboIndex = -1;
+ for (int i : mEventTags) {
+ if (i == mDescriptor.eventTag) {
+ comboIndex = eventIndex;
+ break;
+ }
+ eventIndex++;
+ }
+
+ if (comboIndex == -1) {
+ mEventCombo.clearSelection();
+ } else {
+ mEventCombo.select(comboIndex);
+ }
+
+ // get the event from the descriptor
+ handleEventComboSelection();
+ }
+
+ private void checkValidity() {
+ mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 &&
+ (((mDescriptor instanceof ValueDisplayDescriptor) == false) ||
+ mValueCombo.getSelectionIndex() != -1));
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java
new file mode 100644
index 0000000..3af1447
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.CrosshairState;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.PlotRenderingInfo;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYItemRendererState;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.ui.RectangleEdge;
+
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Stroke;
+import java.awt.geom.Line2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Custom renderer to render event occurrence. This rendered ignores the y value, and simply
+ * draws a line from min to max at the time of the item.
+ */
+public class OccurrenceRenderer extends XYLineAndShapeRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void drawItem(Graphics2D g2,
+ XYItemRendererState state,
+ Rectangle2D dataArea,
+ PlotRenderingInfo info,
+ XYPlot plot,
+ ValueAxis domainAxis,
+ ValueAxis rangeAxis,
+ XYDataset dataset,
+ int series,
+ int item,
+ CrosshairState crosshairState,
+ int pass) {
+ TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset;
+
+ // get the x value for the series/item.
+ double x = timeDataSet.getX(series, item).doubleValue();
+
+ // get the min/max of the range axis
+ double yMin = rangeAxis.getLowerBound();
+ double yMax = rangeAxis.getUpperBound();
+
+ RectangleEdge domainEdge = plot.getDomainAxisEdge();
+ RectangleEdge rangeEdge = plot.getRangeAxisEdge();
+
+ // convert the coordinates to java2d.
+ double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge);
+ double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge);
+ double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge);
+
+ // get the paint information for the series/item
+ Paint p = getItemPaint(series, item);
+ Stroke s = getItemStroke(series, item);
+
+ Line2D line = null;
+ PlotOrientation orientation = plot.getOrientation();
+ if (orientation == PlotOrientation.HORIZONTAL) {
+ line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D);
+ }
+ else if (orientation == PlotOrientation.VERTICAL) {
+ line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D);
+ }
+ g2.setPaint(p);
+ g2.setStroke(s);
+ g2.draw(line);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java
new file mode 100644
index 0000000..0fa6f28
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import java.awt.Color;
+
+abstract public class SyncCommon extends EventDisplay {
+
+ // State information while processing the event stream
+ private int mLastState; // 0 if event started, 1 if event stopped
+ private long mLastStartTime; // ms
+ private long mLastStopTime; //ms
+ private String mLastDetails;
+ private int mLastSyncSource; // poll, server, user, etc.
+
+ // Some common variables for sync display. These define the sync backends
+ //and how they should be displayed.
+ protected static final int CALENDAR = 0;
+ protected static final int GMAIL = 1;
+ protected static final int FEEDS = 2;
+ protected static final int CONTACTS = 3;
+ protected static final int ERRORS = 4;
+ protected static final int NUM_AUTHS = (CONTACTS + 1);
+ protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+ "Errors"};
+ protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+ Color.ORANGE, Color.RED};
+
+ // Values from data/etc/event-log-tags
+ final int EVENT_SYNC = 2720;
+ final int EVENT_TICKLE = 2742;
+ final int EVENT_SYNC_DETAILS = 2743;
+ final int EVENT_CONTACTS_AGGREGATION = 2747;
+
+ protected SyncCommon(String name) {
+ super(name);
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ mLastStartTime = 0;
+ mLastStopTime = 0;
+ mLastState = -1;
+ mLastSyncSource = -1;
+ mLastDetails = "";
+ }
+
+ /**
+ * Updates the display with a new event. This is the main entry point for
+ * each event. This method has the logic to tie together the start event,
+ * stop event, and details event into one graph item. The combined sync event
+ * is handed to the subclass via processSycnEvent. Note that the details
+ * can happen before or after the stop event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ try {
+ if (event.mTag == EVENT_SYNC) {
+ int state = Integer.parseInt(event.getValueAsString(1));
+ if (state == 0) { // start
+ mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ mLastState = 0;
+ mLastSyncSource = Integer.parseInt(event.getValueAsString(2));
+ mLastDetails = "";
+ } else if (state == 1) { // stop
+ if (mLastState == 0) {
+ mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ if (mLastStartTime == 0) {
+ // Log starts with a stop event
+ mLastStartTime = mLastStopTime;
+ }
+ int auth = getAuth(event.getValueAsString(0));
+ processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+ true, mLastSyncSource);
+ mLastState = 1;
+ }
+ }
+ } else if (event.mTag == EVENT_SYNC_DETAILS) {
+ mLastDetails = event.getValueAsString(3);
+ if (mLastState != 0) { // Not inside event
+ long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ if (updateTime - mLastStopTime <= 250) {
+ // Got details within 250ms after event, so delete and re-insert
+ // Details later than 250ms (arbitrary) are discarded as probably
+ // unrelated.
+ int auth = getAuth(event.getValueAsString(0));
+ processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+ false, mLastSyncSource);
+ }
+ }
+ } else if (event.mTag == EVENT_CONTACTS_AGGREGATION) {
+ long stopTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ long startTime = stopTime - Long.parseLong(event.getValueAsString(0));
+ String details;
+ int count = Integer.parseInt(event.getValueAsString(1));
+ if (count < 0) {
+ details = "g" + (-count);
+ } else {
+ details = "G" + count;
+ }
+ processSyncEvent(event, CONTACTS, startTime, stopTime, details,
+ true /* newEvent */, mLastSyncSource);
+ }
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Callback hook for subclass to process a sync event. newEvent has the logic
+ * to combine start and stop events and passes a processed event to the
+ * subclass.
+ *
+ * @param event The sync event
+ * @param auth The sync authority
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource Poll, user, server, etc.
+ */
+ abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource);
+
+ /**
+ * Converts authority name to auth number.
+ *
+ * @param authname "calendar", etc.
+ * @return number series number associated with the authority
+ */
+ protected int getAuth(String authname) throws InvalidTypeException {
+ if ("calendar".equals(authname) || "cl".equals(authname) ||
+ "com.android.calendar".equals(authname)) {
+ return CALENDAR;
+ } else if ("contacts".equals(authname) || "cp".equals(authname) ||
+ "com.android.contacts".equals(authname)) {
+ return CONTACTS;
+ } else if ("subscribedfeeds".equals(authname)) {
+ return FEEDS;
+ } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) {
+ return GMAIL;
+ } else if ("gmail-live".equals(authname)) {
+ return GMAIL;
+ } else if ("unknown".equals(authname)) {
+ return -1; // Unknown tickles; discard
+ } else {
+ throw new InvalidTypeException("Unknown authname " + authname);
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java
new file mode 100644
index 0000000..0e302ce
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmuilib.ImageLoader;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Small dialog box to edit a static port number.
+ */
+public class EditFilterDialog extends Dialog {
+
+ private static final int DLG_WIDTH = 400;
+ private static final int DLG_HEIGHT = 260;
+
+ private static final String IMAGE_WARNING = "warning.png"; //$NON-NLS-1$
+ private static final String IMAGE_EMPTY = "empty.png"; //$NON-NLS-1$
+
+ private Shell mParent;
+
+ private Shell mShell;
+
+ private boolean mOk = false;
+
+ /**
+ * Filter being edited or created
+ */
+ private LogFilter mFilter;
+
+ private String mName;
+ private String mTag;
+ private String mPid;
+
+ /** Log level as an index of the drop-down combo
+ * @see getLogLevel
+ * @see getComboIndex
+ */
+ private int mLogLevel;
+
+ private Button mOkButton;
+
+ private Label mNameWarning;
+ private Label mTagWarning;
+ private Label mPidWarning;
+
+ public EditFilterDialog(Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ }
+
+ public EditFilterDialog(Shell shell, LogFilter filter) {
+ this(shell);
+ mFilter = filter;
+ }
+
+ /**
+ * Opens the dialog. The method will return when the user closes the dialog
+ * somehow.
+ *
+ * @return true if ok was pressed, false if cancelled.
+ */
+ public boolean open() {
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.open();
+
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ // we're quitting with OK.
+ // Lets update the filter if needed
+ if (mOk) {
+ // if it was a "Create filter" action we need to create it first.
+ if (mFilter == null) {
+ mFilter = new LogFilter(mName);
+ }
+
+ // setup the filter
+ mFilter.setTagMode(mTag);
+
+ if (mPid != null && mPid.length() > 0) {
+ mFilter.setPidMode(Integer.parseInt(mPid));
+ } else {
+ mFilter.setPidMode(-1);
+ }
+
+ mFilter.setLogLevel(getLogLevel(mLogLevel));
+ }
+
+ return mOk;
+ }
+
+ public LogFilter getFilter() {
+ return mFilter;
+ }
+
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Log Filter");
+
+ mShell.setLayout(new GridLayout(1, false));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ }
+ });
+
+ // top part with the filter name
+ Composite nameComposite = new Composite(mShell, SWT.NONE);
+ nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ nameComposite.setLayout(new GridLayout(3, false));
+
+ Label l = new Label(nameComposite, SWT.NONE);
+ l.setText("Filter Name:");
+
+ final Text filterNameText = new Text(nameComposite,
+ SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ mName = mFilter.getName();
+ if (mName != null) {
+ filterNameText.setText(mName);
+ }
+ }
+ filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ filterNameText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mName = filterNameText.getText().trim();
+ validate();
+ }
+ });
+
+ mNameWarning = new Label(nameComposite, SWT.NONE);
+ mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+ mShell.getDisplay()));
+
+ // separator
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+
+ // center part with the filter parameters
+ Composite main = new Composite(mShell, SWT.NONE);
+ main.setLayoutData(new GridData(GridData.FILL_BOTH));
+ main.setLayout(new GridLayout(3, false));
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by Log Tag:");
+
+ final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ mTag = mFilter.getTagFilter();
+ if (mTag != null) {
+ tagText.setText(mTag);
+ }
+ }
+
+ tagText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ tagText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mTag = tagText.getText().trim();
+ validate();
+ }
+ });
+
+ mTagWarning = new Label(main, SWT.NONE);
+ mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+ mShell.getDisplay()));
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by pid:");
+
+ final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ if (mFilter.getPidFilter() != -1) {
+ mPid = Integer.toString(mFilter.getPidFilter());
+ } else {
+ mPid = "";
+ }
+ pidText.setText(mPid);
+ }
+ pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ pidText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mPid = pidText.getText().trim();
+ validate();
+ }
+ });
+
+ mPidWarning = new Label(main, SWT.NONE);
+ mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+ mShell.getDisplay()));
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by Log level:");
+
+ final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ logCombo.setLayoutData(gd);
+
+ // add the labels
+ logCombo.add("<none>");
+ logCombo.add("Error");
+ logCombo.add("Warning");
+ logCombo.add("Info");
+ logCombo.add("Debug");
+ logCombo.add("Verbose");
+
+ if (mFilter != null) {
+ mLogLevel = getComboIndex(mFilter.getLogLevel());
+ logCombo.select(mLogLevel);
+ } else {
+ logCombo.select(0);
+ }
+
+ logCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the selection
+ mLogLevel = logCombo.getSelectionIndex();
+ validate();
+ }
+ });
+
+ // separator
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // bottom part with the ok/cancel
+ Composite bottomComp = new Composite(mShell, SWT.NONE);
+ bottomComp
+ .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ bottomComp.setLayout(new GridLayout(2, true));
+
+ mOkButton = new Button(bottomComp, SWT.NONE);
+ mOkButton.setText("OK");
+ mOkButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mOk = true;
+ mShell.close();
+ }
+ });
+ mOkButton.setEnabled(false);
+ mShell.setDefaultButton(mOkButton);
+
+ Button cancelButton = new Button(bottomComp, SWT.NONE);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Returns the log level from a combo index.
+ * @param index the Combo index
+ * @return a log level valid for the Log class.
+ */
+ protected int getLogLevel(int index) {
+ if (index == 0) {
+ return -1;
+ }
+
+ return 7 - index;
+ }
+
+ /**
+ * Returns the index in the combo that matches the log level
+ * @param logLevel The Log level.
+ * @return the combo index
+ */
+ private int getComboIndex(int logLevel) {
+ if (logLevel == -1) {
+ return 0;
+ }
+
+ return 7 - logLevel;
+ }
+
+ /**
+ * Validates the content of the 2 text fields and enable/disable "ok", while
+ * setting up the warning/error message.
+ */
+ private void validate() {
+
+ boolean result = true;
+
+ // then we check it only contains digits.
+ if (mPid != null) {
+ if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$
+ mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_WARNING,
+ mShell.getDisplay()));
+ mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$
+ result = false;
+ } else {
+ mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_EMPTY,
+ mShell.getDisplay()));
+ mPidWarning.setToolTipText(null);
+ }
+ }
+
+ // then we check it not contains character | or :
+ if (mTag != null) {
+ if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$
+ mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_WARNING,
+ mShell.getDisplay()));
+ mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$
+ result = false;
+ } else {
+ mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_EMPTY,
+ mShell.getDisplay()));
+ mTagWarning.setToolTipText(null);
+ }
+ }
+
+ // then we check it not contains character | or :
+ if (mName != null && mName.length() > 0) {
+ if (mName.matches(".*[:|].*") == true) { //$NON-NLS-1$
+ mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_WARNING,
+ mShell.getDisplay()));
+ mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$
+ result = false;
+ } else {
+ mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+ IMAGE_EMPTY,
+ mShell.getDisplay()));
+ mNameWarning.setToolTipText(null);
+ }
+ } else {
+ result = false;
+ }
+
+ mOkButton.setEnabled(result);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java
new file mode 100644
index 0000000..2804629
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.List;
+
+/**
+ * Listeners interested in changes in the logcat buffer should implement this interface.
+ */
+public interface ILogCatBufferChangeListener {
+ /**
+ * Called when the logcat buffer changes.
+ * @param addedMessages list of messages that were added to the logcat buffer
+ * @param deletedMessages list of messages that were removed from the logcat buffer
+ */
+ void bufferChanged(List<LogCatMessage> addedMessages, List<LogCatMessage> deletedMessages);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
new file mode 100644
index 0000000..728b518
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+/**
+ * Classes interested in listening to user selection of logcat
+ * messages should implement this interface.
+ */
+public interface ILogCatMessageSelectionListener {
+ void messageDoubleClicked(LogCatMessage m);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java
new file mode 100644
index 0000000..629b0e0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * A JFace content provider for logcat filter list, used in {@link LogCatPanel}.
+ */
+public final class LogCatFilterContentProvider implements IStructuredContentProvider {
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
+ }
+
+ /**
+ * Obtain the list of filters currently in use.
+ * @param model list of {@link LogCatFilter}'s
+ * @return array of {@link LogCatFilter} objects, or null.
+ */
+ @Override
+ public Object[] getElements(Object model) {
+ return ((List<?>) model).toArray();
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java
new file mode 100644
index 0000000..dbc34d8
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.List;
+
+public class LogCatFilterData {
+ private final LogCatFilter mFilter;
+
+ /** Indicates the number of messages that match this filter, but have not
+ * yet been read by the user. This is really metadata about this filter
+ * necessary for the UI. If we ever end up needing to store more metadata,
+ * then it is probably better to move it out into a separate class. */
+ private int mUnreadCount;
+
+ /** Indicates that this filter is transient, and should not be persisted
+ * across Eclipse sessions. */
+ private boolean mTransient;
+
+ public LogCatFilterData(LogCatFilter f) {
+ mFilter = f;
+
+ // By default, all filters are persistent. Transient filters should explicitly
+ // mark it so by calling setTransient.
+ mTransient = false;
+ }
+
+ /**
+ * Update the unread count based on new messages received. The unread count
+ * is incremented by the count of messages in the received list that will be
+ * accepted by this filter.
+ * @param newMessages list of new messages.
+ */
+ public void updateUnreadCount(List<LogCatMessage> newMessages) {
+ for (LogCatMessage m : newMessages) {
+ if (mFilter.matches(m)) {
+ mUnreadCount++;
+ }
+ }
+ }
+
+ /**
+ * Reset count of unread messages.
+ */
+ public void resetUnreadCount() {
+ mUnreadCount = 0;
+ }
+
+ /**
+ * Get current value for the unread message counter.
+ */
+ public int getUnreadCount() {
+ return mUnreadCount;
+ }
+
+ /** Make this filter transient: It will not be persisted across sessions. */
+ public void setTransient() {
+ mTransient = true;
+ }
+
+ public boolean isTransient() {
+ return mTransient;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java
new file mode 100644
index 0000000..fe24ddd
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.Map;
+
+/**
+ * A JFace label provider for the LogCat filters. It expects elements of type
+ * {@link LogCatFilter}.
+ */
+public final class LogCatFilterLabelProvider extends LabelProvider implements ITableLabelProvider {
+ private Map<LogCatFilter, LogCatFilterData> mFilterData;
+
+ public LogCatFilterLabelProvider(Map<LogCatFilter, LogCatFilterData> filterData) {
+ mFilterData = filterData;
+ }
+
+ @Override
+ public Image getColumnImage(Object arg0, int arg1) {
+ return null;
+ }
+
+ /**
+ * Implements {@link ITableLabelProvider#getColumnText(Object, int)}.
+ * @param element an instance of {@link LogCatFilter}
+ * @param index index of the column
+ * @return text to use in the column
+ */
+ @Override
+ public String getColumnText(Object element, int index) {
+ if (!(element instanceof LogCatFilter)) {
+ return null;
+ }
+
+ LogCatFilter f = (LogCatFilter) element;
+ LogCatFilterData fd = mFilterData.get(f);
+
+ if (fd != null && fd.getUnreadCount() > 0) {
+ return String.format("%s (%d)", f.getName(), fd.getUnreadCount());
+ } else {
+ return f.getName();
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java
new file mode 100644
index 0000000..39b3fa9
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Dialog used to create or edit settings for a logcat filter.
+ */
+public final class LogCatFilterSettingsDialog extends TitleAreaDialog {
+ private static final String TITLE = "Logcat Message Filter Settings";
+ private static final String DEFAULT_MESSAGE =
+ "Filter logcat messages by the source's tag, pid or minimum log level.\n"
+ + "Empty fields will match all messages.";
+
+ private String mFilterName;
+ private String mTag;
+ private String mText;
+ private String mPid;
+ private String mAppName;
+ private String mLogLevel;
+
+ private Text mFilterNameText;
+ private Text mTagFilterText;
+ private Text mTextFilterText;
+ private Text mPidFilterText;
+ private Text mAppNameFilterText;
+ private Combo mLogLevelCombo;
+ private Button mOkButton;
+
+ /**
+ * Construct the filter settings dialog with default values for all fields.
+ * @param parentShell .
+ */
+ public LogCatFilterSettingsDialog(Shell parentShell) {
+ super(parentShell);
+ setDefaults("", "", "", "", "", LogLevel.VERBOSE);
+ }
+
+ /**
+ * Set the default values to show when the dialog is opened.
+ * @param filterName name for the filter.
+ * @param tag value for filter by tag
+ * @param text value for filter by text
+ * @param pid value for filter by pid
+ * @param appName value for filter by app name
+ * @param level value for filter by log level
+ */
+ public void setDefaults(String filterName, String tag, String text, String pid, String appName,
+ LogLevel level) {
+ mFilterName = filterName;
+ mTag = tag;
+ mText = text;
+ mPid = pid;
+ mAppName = appName;
+ mLogLevel = level.getStringValue();
+ }
+
+ @Override
+ protected Control createDialogArea(Composite shell) {
+ setTitle(TITLE);
+ setMessage(DEFAULT_MESSAGE);
+
+ Composite parent = (Composite) super.createDialogArea(shell);
+ Composite c = new Composite(parent, SWT.BORDER);
+ c.setLayout(new GridLayout(2, false));
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createLabel(c, "Filter Name:");
+ mFilterNameText = new Text(c, SWT.BORDER);
+ mFilterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterNameText.setText(mFilterName);
+
+ createSeparator(c);
+
+ createLabel(c, "by Log Tag:");
+ mTagFilterText = new Text(c, SWT.BORDER);
+ mTagFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTagFilterText.setText(mTag);
+
+ createLabel(c, "by Log Message:");
+ mTextFilterText = new Text(c, SWT.BORDER);
+ mTextFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTextFilterText.setText(mText);
+
+ createLabel(c, "by PID:");
+ mPidFilterText = new Text(c, SWT.BORDER);
+ mPidFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mPidFilterText.setText(mPid);
+
+ createLabel(c, "by Application Name:");
+ mAppNameFilterText = new Text(c, SWT.BORDER);
+ mAppNameFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mAppNameFilterText.setText(mAppName);
+
+ createLabel(c, "by Log Level:");
+ mLogLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN);
+ mLogLevelCombo.setItems(getLogLevels().toArray(new String[0]));
+ mLogLevelCombo.select(getLogLevels().indexOf(mLogLevel));
+
+ /* call validateDialog() whenever user modifies any text field */
+ ModifyListener m = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ DialogStatus status = validateDialog();
+ mOkButton.setEnabled(status.valid);
+ setErrorMessage(status.message);
+ }
+ };
+ mFilterNameText.addModifyListener(m);
+ mTagFilterText.addModifyListener(m);
+ mTextFilterText.addModifyListener(m);
+ mPidFilterText.addModifyListener(m);
+ mAppNameFilterText.addModifyListener(m);
+
+ return c;
+ }
+
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ super.createButtonsForButtonBar(parent);
+
+ mOkButton = getButton(IDialogConstants.OK_ID);
+
+ DialogStatus status = validateDialog();
+ mOkButton.setEnabled(status.valid);
+ }
+
+ /**
+ * A tuple that specifies whether the current state of the inputs
+ * on the dialog is valid or not. If it is not valid, the message
+ * field stores the reason why it isn't.
+ */
+ private static final class DialogStatus {
+ final boolean valid;
+ final String message;
+
+ private DialogStatus(boolean isValid, String errMessage) {
+ valid = isValid;
+ message = errMessage;
+ }
+ }
+
+ private DialogStatus validateDialog() {
+ /* check that there is some name for the filter */
+ if (mFilterNameText.getText().trim().equals("")) {
+ return new DialogStatus(false,
+ "Please provide a name for this filter.");
+ }
+
+ /* if a pid is provided, it should be a +ve integer */
+ String pidText = mPidFilterText.getText().trim();
+ if (pidText.trim().length() > 0) {
+ int pid = 0;
+ try {
+ pid = Integer.parseInt(pidText);
+ } catch (NumberFormatException e) {
+ return new DialogStatus(false,
+ "PID should be a positive integer.");
+ }
+
+ if (pid < 0) {
+ return new DialogStatus(false,
+ "PID should be a positive integer.");
+ }
+ }
+
+ /* tag field must use a valid regex pattern */
+ String tagText = mTagFilterText.getText().trim();
+ if (tagText.trim().length() > 0) {
+ try {
+ Pattern.compile(tagText);
+ } catch (PatternSyntaxException e) {
+ return new DialogStatus(false,
+ "Invalid regex used in tag field: " + e.getMessage());
+ }
+ }
+
+ /* text field must use a valid regex pattern */
+ String messageText = mTextFilterText.getText().trim();
+ if (messageText.trim().length() > 0) {
+ try {
+ Pattern.compile(messageText);
+ } catch (PatternSyntaxException e) {
+ return new DialogStatus(false,
+ "Invalid regex used in text field: " + e.getMessage());
+ }
+ }
+
+ /* app name field must use a valid regex pattern */
+ String appNameText = mAppNameFilterText.getText().trim();
+ if (appNameText.trim().length() > 0) {
+ try {
+ Pattern.compile(appNameText);
+ } catch (PatternSyntaxException e) {
+ return new DialogStatus(false,
+ "Invalid regex used in application name field: " + e.getMessage());
+ }
+ }
+
+ return new DialogStatus(true, null);
+ }
+
+ private void createSeparator(Composite c) {
+ Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+ }
+
+ private void createLabel(Composite c, String text) {
+ Label l = new Label(c, SWT.NONE);
+ l.setText(text);
+ GridData gd = new GridData();
+ gd.horizontalAlignment = SWT.RIGHT;
+ l.setLayoutData(gd);
+ }
+
+ @Override
+ protected void okPressed() {
+ /* save values from the widgets before the shell is closed. */
+ mFilterName = mFilterNameText.getText();
+ mTag = mTagFilterText.getText();
+ mText = mTextFilterText.getText();
+ mLogLevel = mLogLevelCombo.getText();
+ mPid = mPidFilterText.getText();
+ mAppName = mAppNameFilterText.getText();
+
+ super.okPressed();
+ }
+
+ /**
+ * Obtain the name for this filter.
+ * @return user provided filter name, maybe empty.
+ */
+ public String getFilterName() {
+ return mFilterName;
+ }
+
+ /**
+ * Obtain the tag regex to filter by.
+ * @return user provided tag regex, maybe empty.
+ */
+ public String getTag() {
+ return mTag;
+ }
+
+ /**
+ * Obtain the text regex to filter by.
+ * @return user provided tag regex, maybe empty.
+ */
+ public String getText() {
+ return mText;
+ }
+
+ /**
+ * Obtain user provided PID to filter by.
+ * @return user provided pid, maybe empty.
+ */
+ public String getPid() {
+ return mPid;
+ }
+
+ /**
+ * Obtain user provided application name to filter by.
+ * @return user provided app name regex, maybe empty
+ */
+ public String getAppName() {
+ return mAppName;
+ }
+
+ /**
+ * Obtain log level to filter by.
+ * @return log level string.
+ */
+ public String getLogLevel() {
+ return mLogLevel;
+ }
+
+ /**
+ * Obtain the string representation of all supported log levels.
+ * @return an array of strings, each representing a certain log level.
+ */
+ public static List<String> getLogLevels() {
+ List<String> logLevels = new ArrayList<String>();
+
+ for (LogLevel l : LogLevel.values()) {
+ logLevels.add(l.getStringValue());
+ }
+
+ return logLevels;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java
new file mode 100644
index 0000000..de35162
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class to help save/restore user created filters.
+ *
+ * Users can create multiple filters in the logcat view. These filters could have regexes
+ * in their settings. All of the user created filters are saved into a single Eclipse
+ * preference. This class helps in generating the string to be saved given a list of
+ * {@link LogCatFilter}'s, and also does the reverse of creating the list of filters
+ * given the encoded string.
+ */
+public final class LogCatFilterSettingsSerializer {
+ private static final char SINGLE_QUOTE = '\'';
+ private static final char ESCAPE_CHAR = '\\';
+
+ private static final String ATTR_DELIM = ", ";
+ private static final String KW_DELIM = ": ";
+
+ private static final String KW_NAME = "name";
+ private static final String KW_TAG = "tag";
+ private static final String KW_TEXT = "text";
+ private static final String KW_PID = "pid";
+ private static final String KW_APP = "app";
+ private static final String KW_LOGLEVEL = "level";
+
+ /**
+ * Encode the settings from a list of {@link LogCatFilter}'s into a string for saving to
+ * the preference store. See
+ * {@link LogCatFilterSettingsSerializer#decodeFromPreferenceString(String)} for the
+ * reverse operation.
+ * @param filters list of filters to save.
+ * @param filterData mapping from filter to per filter UI data
+ * @return an encoded string that can be saved in Eclipse preference store. The encoded string
+ * is of a list of key:'value' pairs.
+ */
+ public String encodeToPreferenceString(List<LogCatFilter> filters,
+ Map<LogCatFilter, LogCatFilterData> filterData) {
+ StringBuffer sb = new StringBuffer();
+
+ for (LogCatFilter f : filters) {
+ LogCatFilterData fd = filterData.get(f);
+ if (fd != null && fd.isTransient()) {
+ // do not persist transient filters
+ continue;
+ }
+
+ sb.append(KW_NAME); sb.append(KW_DELIM); sb.append(quoteString(f.getName()));
+ sb.append(ATTR_DELIM);
+ sb.append(KW_TAG); sb.append(KW_DELIM); sb.append(quoteString(f.getTag()));
+ sb.append(ATTR_DELIM);
+ sb.append(KW_TEXT); sb.append(KW_DELIM); sb.append(quoteString(f.getText()));
+ sb.append(ATTR_DELIM);
+ sb.append(KW_PID); sb.append(KW_DELIM); sb.append(quoteString(f.getPid()));
+ sb.append(ATTR_DELIM);
+ sb.append(KW_APP); sb.append(KW_DELIM); sb.append(quoteString(f.getAppName()));
+ sb.append(ATTR_DELIM);
+ sb.append(KW_LOGLEVEL); sb.append(KW_DELIM);
+ sb.append(quoteString(f.getLogLevel().getStringValue()));
+ sb.append(ATTR_DELIM);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Decode an encoded string representing the settings of a list of logcat
+ * filters into a list of {@link LogCatFilter}'s.
+ * @param pref encoded preference string
+ * @return a list of {@link LogCatFilter}
+ */
+ public List<LogCatFilter> decodeFromPreferenceString(String pref) {
+ List<LogCatFilter> fs = new ArrayList<LogCatFilter>();
+
+ /* first split the string into a list of key, value pairs */
+ List<String> kv = getKeyValues(pref);
+ if (kv.size() == 0) {
+ return fs;
+ }
+
+ /* construct filter settings from the key value pairs */
+ int index = 0;
+ while (index < kv.size()) {
+ String name = "";
+ String tag = "";
+ String pid = "";
+ String app = "";
+ String text = "";
+ LogLevel level = LogLevel.VERBOSE;
+
+ assert kv.get(index).equals(KW_NAME);
+ name = kv.get(index + 1);
+
+ index += 2;
+ while (index < kv.size() && !kv.get(index).equals(KW_NAME)) {
+ String key = kv.get(index);
+ String value = kv.get(index + 1);
+ index += 2;
+
+ if (key.equals(KW_TAG)) {
+ tag = value;
+ } else if (key.equals(KW_TEXT)) {
+ text = value;
+ } else if (key.equals(KW_PID)) {
+ pid = value;
+ } else if (key.equals(KW_APP)) {
+ app = value;
+ } else if (key.equals(KW_LOGLEVEL)) {
+ level = LogLevel.getByString(value);
+ }
+ }
+
+ fs.add(new LogCatFilter(name, tag, text, pid, app, level));
+ }
+
+ return fs;
+ }
+
+ private List<String> getKeyValues(String pref) {
+ List<String> kv = new ArrayList<String>();
+ int index = 0;
+ while (index < pref.length()) {
+ String kw = getKeyword(pref.substring(index));
+ if (kw == null) {
+ break;
+ }
+ index += kw.length() + KW_DELIM.length();
+
+ String value = getNextString(pref.substring(index));
+ index += value.length() + ATTR_DELIM.length();
+
+ value = unquoteString(value);
+
+ kv.add(kw);
+ kv.add(value);
+ }
+
+ return kv;
+ }
+
+ /**
+ * Enclose a string in quotes, escaping all the quotes within the string.
+ */
+ private String quoteString(String s) {
+ return SINGLE_QUOTE + s.replace(Character.toString(SINGLE_QUOTE), "\\'")
+ + SINGLE_QUOTE;
+ }
+
+ /**
+ * Recover original string from its escaped version created using
+ * {@link LogCatFilterSettingsSerializer#quoteString(String)}.
+ */
+ private String unquoteString(String s) {
+ s = s.substring(1, s.length() - 1); /* remove start and end QUOTES */
+ return s.replace("\\'", Character.toString(SINGLE_QUOTE));
+ }
+
+ private String getKeyword(String pref) {
+ int kwlen = pref.indexOf(KW_DELIM);
+ if (kwlen == -1) {
+ return null;
+ }
+
+ return pref.substring(0, kwlen);
+ }
+
+ /**
+ * Get the next quoted string from the input stream of characters.
+ */
+ private String getNextString(String s) {
+ assert s.charAt(0) == SINGLE_QUOTE;
+
+ StringBuffer sb = new StringBuffer();
+
+ int index = 0;
+ while (index < s.length()) {
+ sb.append(s.charAt(index));
+
+ if (index > 0
+ && s.charAt(index) == SINGLE_QUOTE // current char is a single quote
+ && s.charAt(index - 1) != ESCAPE_CHAR) { // prev char wasn't a backslash
+ /* break if an unescaped SINGLE QUOTE (end of string) is seen */
+ break;
+ }
+
+ index++;
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java
new file mode 100644
index 0000000..c5cd548
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Container for a list of log messages. The list of messages are
+ * maintained in a circular buffer (FIFO).
+ */
+public final class LogCatMessageList {
+ /** Preference key for size of the FIFO. */
+ public static final String MAX_MESSAGES_PREFKEY =
+ "logcat.messagelist.max.size";
+
+ /** Default value for max # of messages. */
+ public static final int MAX_MESSAGES_DEFAULT = 5000;
+
+ private int mFifoSize;
+ private BlockingQueue<LogCatMessage> mQ;
+
+ /**
+ * Construct an empty message list.
+ * @param maxMessages capacity of the circular buffer
+ */
+ public LogCatMessageList(int maxMessages) {
+ mFifoSize = maxMessages;
+
+ mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize);
+ }
+
+ /**
+ * Resize the message list.
+ * @param n new size for the list
+ */
+ public synchronized void resize(int n) {
+ mFifoSize = n;
+
+ if (mFifoSize > mQ.size()) {
+ /* if resizing to a bigger fifo, we can copy over all elements from the current mQ */
+ mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize, true, mQ);
+ } else {
+ /* for a smaller fifo, copy over the last n entries */
+ LogCatMessage[] curMessages = mQ.toArray(new LogCatMessage[mQ.size()]);
+ mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize);
+ for (int i = curMessages.length - mFifoSize; i < curMessages.length; i++) {
+ mQ.offer(curMessages[i]);
+ }
+ }
+ }
+
+ /**
+ * Append a message to the list. If the list is full, the first
+ * message will be popped off of it.
+ * @param m log to be inserted
+ */
+ public synchronized void appendMessages(final List<LogCatMessage> messages) {
+ ensureSpace(messages.size());
+ for (LogCatMessage m: messages) {
+ mQ.offer(m);
+ }
+ }
+
+ /**
+ * Ensure that there is sufficient space for given number of messages.
+ * @return list of messages that were deleted to create additional space.
+ */
+ public synchronized List<LogCatMessage> ensureSpace(int messageCount) {
+ List<LogCatMessage> l = new ArrayList<LogCatMessage>(messageCount);
+
+ while (mQ.remainingCapacity() < messageCount) {
+ l.add(mQ.poll());
+ }
+
+ return l;
+ }
+
+ /**
+ * Returns the number of additional elements that this queue can
+ * ideally (in the absence of memory or resource constraints)
+ * accept without blocking.
+ * @return the remaining capacity
+ */
+ public synchronized int remainingCapacity() {
+ return mQ.remainingCapacity();
+ }
+
+ /** Clear all messages in the list. */
+ public synchronized void clear() {
+ mQ.clear();
+ }
+
+ /** Obtain a copy of the message list. */
+ public synchronized List<LogCatMessage> getAllMessages() {
+ return new ArrayList<LogCatMessage>(mQ);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java
new file mode 100644
index 0000000..bda742c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java
@@ -0,0 +1,1607 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatFilter;
+import com.android.ddmlib.logcat.LogCatMessage;
+import com.android.ddmuilib.AbstractBufferFindTarget;
+import com.android.ddmuilib.FindDialog;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferenceConverter;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * LogCatPanel displays a table listing the logcat messages.
+ */
+public final class LogCatPanel extends SelectionDependentPanel
+ implements ILogCatBufferChangeListener {
+ /** Preference key to use for storing list of logcat filters. */
+ public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list";
+
+ /** Preference key to use for storing font settings. */
+ public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font";
+
+ /** Preference key to use for deciding whether to automatically en/disable scroll lock. */
+ public static final String AUTO_SCROLL_LOCK_PREFKEY = "logcat.view.auto-scroll-lock";
+
+ // Preference keys for message colors based on severity level
+ private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color.";
+ public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$
+ public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$
+ public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$
+ public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$
+ public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$
+ public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$
+
+ // Use a monospace font family
+ private static final String FONT_FAMILY =
+ DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New";
+
+ // Use the default system font size
+ private static final FontData DEFAULT_LOGCAT_FONTDATA;
+ static {
+ int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight();
+ DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL);
+ }
+
+ private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize.";
+ private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters";
+
+ /** Default message to show in the message search field. */
+ private static final String DEFAULT_SEARCH_MESSAGE =
+ "Search for messages. Accepts Java regexes. "
+ + "Prefix with pid:, app:, tag: or text: to limit scope.";
+
+ /** Tooltip to show in the message search field. */
+ private static final String DEFAULT_SEARCH_TOOLTIP =
+ "Example search patterns:\n"
+ + " sqlite (search for sqlite in text field)\n"
+ + " app:browser (search for messages generated by the browser application)";
+
+ private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$
+ private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$
+ private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$
+ private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$
+ private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$
+ private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$
+ private static final String IMAGE_SCROLL_LOCK = "scroll_lock.png"; //$NON-NLS-1$
+
+ private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85};
+ private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100};
+
+ /** Index of the default filter in the saved filters column. */
+ private static final int DEFAULT_FILTER_INDEX = 0;
+
+ /* Text colors for the filter box */
+ private static final Color VALID_FILTER_REGEX_COLOR =
+ Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
+ private static final Color INVALID_FILTER_REGEX_COLOR =
+ Display.getDefault().getSystemColor(SWT.COLOR_RED);
+
+ private LogCatReceiver mReceiver;
+ private IPreferenceStore mPrefStore;
+
+ private List<LogCatFilter> mLogCatFilters;
+ private Map<LogCatFilter, LogCatFilterData> mLogCatFilterData;
+ private int mCurrentSelectedFilterIndex;
+
+ private ToolItem mNewFilterToolItem;
+ private ToolItem mDeleteFilterToolItem;
+ private ToolItem mEditFilterToolItem;
+ private TableViewer mFiltersTableViewer;
+
+ private Combo mLiveFilterLevelCombo;
+ private Text mLiveFilterText;
+
+ private List<LogCatFilter> mCurrentFilters = Collections.emptyList();
+
+ private Table mTable;
+
+ private boolean mShouldScrollToLatestLog = true;
+ private ToolItem mScrollLockCheckBox;
+ private boolean mAutoScrollLock;
+
+ // Lock under which the vertical scroll bar listener should be added
+ private final Object mScrollBarSelectionListenerLock = new Object();
+ private SelectionListener mScrollBarSelectionListener;
+ private boolean mScrollBarListenerSet = false;
+
+ private String mLogFileExportFolder;
+
+ private Font mFont;
+ private int mWrapWidthInChars;
+
+ private Color mVerboseColor;
+ private Color mDebugColor;
+ private Color mInfoColor;
+ private Color mWarnColor;
+ private Color mErrorColor;
+ private Color mAssertColor;
+
+ private SashForm mSash;
+
+ // messages added since last refresh, synchronized on mLogBuffer
+ private List<LogCatMessage> mLogBuffer;
+
+ // # of messages deleted since last refresh, synchronized on mLogBuffer
+ private int mDeletedLogCount;
+
+ /**
+ * Construct a logcat panel.
+ * @param prefStore preference store where UI preferences will be saved
+ */
+ public LogCatPanel(IPreferenceStore prefStore) {
+ mPrefStore = prefStore;
+ mLogBuffer = new ArrayList<LogCatMessage>(LogCatMessageList.MAX_MESSAGES_DEFAULT);
+
+ initializeFilters();
+
+ setupDefaultPreferences();
+ initializePreferenceUpdateListeners();
+
+ mFont = getFontFromPrefStore();
+ loadMessageColorPreferences();
+ mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY);
+ }
+
+ private void loadMessageColorPreferences() {
+ if (mVerboseColor != null) {
+ disposeMessageColors();
+ }
+
+ mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY);
+ mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY);
+ mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY);
+ mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY);
+ mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY);
+ mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY);
+ }
+
+ private void initializeFilters() {
+ mLogCatFilters = new ArrayList<LogCatFilter>();
+ mLogCatFilterData = new ConcurrentHashMap<LogCatFilter, LogCatFilterData>();
+
+ /* add default filter matching all messages */
+ String tag = "";
+ String text = "";
+ String pid = "";
+ String app = "";
+ LogCatFilter defaultFilter = new LogCatFilter("All messages (no filters)",
+ tag, text, pid, app, LogLevel.VERBOSE);
+
+ mLogCatFilters.add(defaultFilter);
+ mLogCatFilterData.put(defaultFilter, new LogCatFilterData(defaultFilter));
+
+ /* restore saved filters from prefStore */
+ List<LogCatFilter> savedFilters = getSavedFilters();
+ for (LogCatFilter f: savedFilters) {
+ mLogCatFilters.add(f);
+ mLogCatFilterData.put(f, new LogCatFilterData(f));
+ }
+ }
+
+ private void setupDefaultPreferences() {
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY,
+ DEFAULT_LOGCAT_FONTDATA);
+ mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY,
+ LogCatMessageList.MAX_MESSAGES_DEFAULT);
+ mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true);
+ mPrefStore.setDefault(AUTO_SCROLL_LOCK_PREFKEY, true);
+
+ /* Default Colors for different log levels. */
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY,
+ new RGB(0, 0, 0));
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY,
+ new RGB(0, 0, 127));
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY,
+ new RGB(0, 127, 0));
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY,
+ new RGB(255, 127, 0));
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY,
+ new RGB(255, 0, 0));
+ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY,
+ new RGB(255, 0, 0));
+ }
+
+ private void initializePreferenceUpdateListeners() {
+ mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ String changedProperty = event.getProperty();
+ if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) {
+ if (mFont != null) {
+ mFont.dispose();
+ }
+ mFont = getFontFromPrefStore();
+ recomputeWrapWidth();
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ for (TableItem it: mTable.getItems()) {
+ it.setFont(mFont);
+ }
+ }
+ });
+ } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) {
+ loadMessageColorPreferences();
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ Color c = mVerboseColor;
+ for (TableItem it: mTable.getItems()) {
+ Object data = it.getData();
+ if (data instanceof LogCatMessage) {
+ c = getForegroundColor((LogCatMessage) data);
+ }
+ it.setForeground(c);
+ }
+ }
+ });
+ } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) {
+ mReceiver.resizeFifo(mPrefStore.getInt(
+ LogCatMessageList.MAX_MESSAGES_PREFKEY));
+ reloadLogBuffer();
+ } else if (changedProperty.equals(AUTO_SCROLL_LOCK_PREFKEY)) {
+ mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY);
+ }
+ }
+ });
+ }
+
+ private void saveFilterPreferences() {
+ LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+
+ /* save all filter settings except the first one which is the default */
+ String e = serializer.encodeToPreferenceString(
+ mLogCatFilters.subList(1, mLogCatFilters.size()), mLogCatFilterData);
+ mPrefStore.setValue(LOGCAT_FILTERS_LIST, e);
+ }
+
+ private List<LogCatFilter> getSavedFilters() {
+ LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+ String e = mPrefStore.getString(LOGCAT_FILTERS_LIST);
+ return serializer.decodeFromPreferenceString(e);
+ }
+
+ @Override
+ public void deviceSelected() {
+ IDevice device = getCurrentDevice();
+ if (device == null) {
+ // If the device is not working properly, getCurrentDevice() could return null.
+ // In such a case, we don't launch logcat, nor switch the display.
+ return;
+ }
+
+ if (mReceiver != null) {
+ // Don't need to listen to new logcat messages from previous device anymore.
+ mReceiver.removeMessageReceivedEventListener(this);
+
+ // When switching between devices, existing filter match count should be reset.
+ for (LogCatFilter f : mLogCatFilters) {
+ LogCatFilterData fd = mLogCatFilterData.get(f);
+ fd.resetUnreadCount();
+ }
+ }
+
+ mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore);
+ mReceiver.addMessageReceivedEventListener(this);
+ reloadLogBuffer();
+
+ // Always scroll to last line whenever the selected device changes.
+ // Run this in a separate async thread to give the table some time to update after the
+ // setInput above.
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ scrollToLatestLog();
+ }
+ });
+ }
+
+ @Override
+ public void clientSelected() {
+ }
+
+ @Override
+ protected void postCreation() {
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ GridLayout layout = new GridLayout(1, false);
+ parent.setLayout(layout);
+
+ createViews(parent);
+ setupDefaults();
+
+ return null;
+ }
+
+ private void createViews(Composite parent) {
+ mSash = createSash(parent);
+
+ createListOfFilters(mSash);
+ createLogTableView(mSash);
+
+ boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY);
+ updateFiltersColumn(showFilters);
+ }
+
+ private SashForm createSash(Composite parent) {
+ SashForm sash = new SashForm(parent, SWT.HORIZONTAL);
+ sash.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return sash;
+ }
+
+ private void createListOfFilters(SashForm sash) {
+ Composite c = new Composite(sash, SWT.BORDER);
+ GridLayout layout = new GridLayout(2, false);
+ c.setLayout(layout);
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createFiltersToolbar(c);
+ createFiltersTable(c);
+ }
+
+ private void createFiltersToolbar(Composite parent) {
+ Label l = new Label(parent, SWT.NONE);
+ l.setText("Saved Filters");
+ GridData gd = new GridData();
+ gd.horizontalAlignment = SWT.LEFT;
+ l.setLayoutData(gd);
+
+ ToolBar t = new ToolBar(parent, SWT.FLAT);
+ gd = new GridData();
+ gd.horizontalAlignment = SWT.RIGHT;
+ t.setLayoutData(gd);
+
+ /* new filter */
+ mNewFilterToolItem = new ToolItem(t, SWT.PUSH);
+ mNewFilterToolItem.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay()));
+ mNewFilterToolItem.setToolTipText("Add a new logcat filter");
+ mNewFilterToolItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ addNewFilter();
+ }
+ });
+
+ /* delete filter */
+ mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH);
+ mDeleteFilterToolItem.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay()));
+ mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter");
+ mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ deleteSelectedFilter();
+ }
+ });
+
+ /* edit filter */
+ mEditFilterToolItem = new ToolItem(t, SWT.PUSH);
+ mEditFilterToolItem.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay()));
+ mEditFilterToolItem.setToolTipText("Edit selected logcat filter");
+ mEditFilterToolItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ editSelectedFilter();
+ }
+ });
+ }
+
+ private void addNewFilter(String defaultTag, String defaultText, String defaultPid,
+ String defaultAppName, LogLevel defaultLevel) {
+ LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog(
+ Display.getCurrent().getActiveShell());
+ d.setDefaults("", defaultTag, defaultText, defaultPid, defaultAppName, defaultLevel);
+ if (d.open() != Window.OK) {
+ return;
+ }
+
+ LogCatFilter f = new LogCatFilter(d.getFilterName().trim(),
+ d.getTag().trim(),
+ d.getText().trim(),
+ d.getPid().trim(),
+ d.getAppName().trim(),
+ LogLevel.getByString(d.getLogLevel()));
+
+ mLogCatFilters.add(f);
+ mLogCatFilterData.put(f, new LogCatFilterData(f));
+ mFiltersTableViewer.refresh();
+
+ /* select the newly added entry */
+ int idx = mLogCatFilters.size() - 1;
+ mFiltersTableViewer.getTable().setSelection(idx);
+
+ filterSelectionChanged();
+ saveFilterPreferences();
+ }
+
+ private void addNewFilter() {
+ addNewFilter("", "", "", "", LogLevel.VERBOSE);
+ }
+
+ private void deleteSelectedFilter() {
+ int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex();
+ if (selectedIndex <= 0) {
+ /* return if no selected filter, or the default filter was selected (0th). */
+ return;
+ }
+
+ LogCatFilter f = mLogCatFilters.get(selectedIndex);
+ mLogCatFilters.remove(selectedIndex);
+ mLogCatFilterData.remove(f);
+
+ mFiltersTableViewer.refresh();
+ mFiltersTableViewer.getTable().setSelection(selectedIndex - 1);
+
+ filterSelectionChanged();
+ saveFilterPreferences();
+ }
+
+ private void editSelectedFilter() {
+ int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex();
+ if (selectedIndex < 0) {
+ return;
+ }
+
+ LogCatFilter curFilter = mLogCatFilters.get(selectedIndex);
+
+ LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog(
+ Display.getCurrent().getActiveShell());
+ dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(),
+ curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel());
+ if (dialog.open() != Window.OK) {
+ return;
+ }
+
+ LogCatFilter f = new LogCatFilter(dialog.getFilterName(),
+ dialog.getTag(),
+ dialog.getText(),
+ dialog.getPid(),
+ dialog.getAppName(),
+ LogLevel.getByString(dialog.getLogLevel()));
+ mLogCatFilters.set(selectedIndex, f);
+ mFiltersTableViewer.refresh();
+
+ mFiltersTableViewer.getTable().setSelection(selectedIndex);
+ filterSelectionChanged();
+ saveFilterPreferences();
+ }
+
+ /**
+ * Select the transient filter for the specified application. If no such filter
+ * exists, then create one and then select that. This method should be called from
+ * the UI thread.
+ * @param appName application name to filter by
+ */
+ public void selectTransientAppFilter(String appName) {
+ assert mTable.getDisplay().getThread() == Thread.currentThread();
+
+ LogCatFilter f = findTransientAppFilter(appName);
+ if (f == null) {
+ f = createTransientAppFilter(appName);
+ mLogCatFilters.add(f);
+
+ LogCatFilterData fd = new LogCatFilterData(f);
+ fd.setTransient();
+ mLogCatFilterData.put(f, fd);
+ }
+
+ selectFilterAt(mLogCatFilters.indexOf(f));
+ }
+
+ private LogCatFilter findTransientAppFilter(String appName) {
+ for (LogCatFilter f : mLogCatFilters) {
+ LogCatFilterData fd = mLogCatFilterData.get(f);
+ if (fd != null && fd.isTransient() && f.getAppName().equals(appName)) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ private LogCatFilter createTransientAppFilter(String appName) {
+ LogCatFilter f = new LogCatFilter(appName + " (Session Filter)",
+ "",
+ "",
+ "",
+ appName,
+ LogLevel.VERBOSE);
+ return f;
+ }
+
+ private void selectFilterAt(final int index) {
+ mFiltersTableViewer.refresh();
+
+ if (index != mFiltersTableViewer.getTable().getSelectionIndex()) {
+ mFiltersTableViewer.getTable().setSelection(index);
+ filterSelectionChanged();
+ }
+ }
+
+ private void createFiltersTable(Composite parent) {
+ final Table table = new Table(parent, SWT.FULL_SELECTION);
+
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ gd.horizontalSpan = 2;
+ table.setLayoutData(gd);
+
+ mFiltersTableViewer = new TableViewer(table);
+ mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider());
+ mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider(mLogCatFilterData));
+ mFiltersTableViewer.setInput(mLogCatFilters);
+
+ mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ filterSelectionChanged();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent arg0) {
+ editSelectedFilter();
+ }
+ });
+ }
+
+ private void createLogTableView(SashForm sash) {
+ Composite c = new Composite(sash, SWT.NONE);
+ c.setLayout(new GridLayout());
+ c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createLiveFilters(c);
+ createLogcatViewTable(c);
+ }
+
+ /** Create the search bar at the top of the logcat messages table. */
+ private void createLiveFilters(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(3, false));
+ c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH);
+ mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE);
+ mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP);
+ mLiveFilterText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent arg0) {
+ updateFilterTextColor();
+ updateAppliedFilters();
+ }
+ });
+
+ mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN);
+ mLiveFilterLevelCombo.setItems(
+ LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0]));
+ mLiveFilterLevelCombo.select(0);
+ mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ updateAppliedFilters();
+ }
+ });
+
+ ToolBar toolBar = new ToolBar(c, SWT.FLAT);
+
+ ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH);
+ saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE,
+ toolBar.getDisplay()));
+ saveToLog.setToolTipText("Export Selected Items To Text File..");
+ saveToLog.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ saveLogToFile();
+ }
+ });
+
+ ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH);
+ clearLog.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay()));
+ clearLog.setToolTipText("Clear Log");
+ clearLog.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent arg0) {
+ if (mReceiver != null) {
+ mReceiver.clearMessages();
+ refreshLogCatTable();
+ resetUnreadCountForAllFilters();
+
+ // the filters view is not cleared unless the filters are re-applied.
+ updateAppliedFilters();
+ }
+ }
+ });
+
+ final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK);
+ showFiltersColumn.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS,
+ toolBar.getDisplay()));
+ showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY));
+ showFiltersColumn.setToolTipText("Display Saved Filters View");
+ showFiltersColumn.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ boolean showFilters = showFiltersColumn.getSelection();
+ mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters);
+ updateFiltersColumn(showFilters);
+ }
+ });
+
+ mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK);
+ mScrollLockCheckBox.setImage(
+ ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK,
+ toolBar.getDisplay()));
+ mScrollLockCheckBox.setSelection(true);
+ mScrollLockCheckBox.setToolTipText("Scroll Lock");
+ mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ boolean scrollLock = mScrollLockCheckBox.getSelection();
+ setScrollToLatestLog(scrollLock);
+ }
+ });
+ }
+
+ /** Sets the foreground color of filter text based on whether the regex is valid. */
+ private void updateFilterTextColor() {
+ String text = mLiveFilterText.getText();
+ Color c;
+ try {
+ Pattern.compile(text.trim());
+ c = VALID_FILTER_REGEX_COLOR;
+ } catch (PatternSyntaxException e) {
+ c = INVALID_FILTER_REGEX_COLOR;
+ }
+ mLiveFilterText.setForeground(c);
+ }
+
+ private void updateFiltersColumn(boolean showFilters) {
+ if (showFilters) {
+ mSash.setWeights(WEIGHTS_SHOW_FILTERS);
+ } else {
+ mSash.setWeights(WEIGHTS_LOGCAT_ONLY);
+ }
+ }
+
+ /**
+ * Save logcat messages selected in the table to a file.
+ */
+ private void saveLogToFile() {
+ /* show dialog box and get target file name */
+ final String fName = getLogFileTargetLocation();
+ if (fName == null) {
+ return;
+ }
+
+ /* obtain list of selected messages */
+ final List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+
+ /* save messages to file in a different (non UI) thread */
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ BufferedWriter w = null;
+ try {
+ w = new BufferedWriter(new FileWriter(fName));
+ for (LogCatMessage m : selectedMessages) {
+ w.append(m.toString());
+ w.newLine();
+ }
+ } catch (final IOException e) {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ MessageDialog.openError(Display.getCurrent().getActiveShell(),
+ "Unable to export selection to file.",
+ "Unexpected error while saving selected messages to file: "
+ + e.getMessage());
+ }
+ });
+ } finally {
+ if (w != null) {
+ try {
+ w.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+ });
+ t.setName("Saving selected items to logfile..");
+ t.start();
+ }
+
+ /**
+ * Display a {@link FileDialog} to the user and obtain the location for the log file.
+ * @return path to target file, null if user canceled the dialog
+ */
+ private String getLogFileTargetLocation() {
+ FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE);
+
+ fd.setText("Save Log..");
+ fd.setFileName("log.txt");
+
+ if (mLogFileExportFolder == null) {
+ mLogFileExportFolder = System.getProperty("user.home");
+ }
+ fd.setFilterPath(mLogFileExportFolder);
+
+ fd.setFilterNames(new String[] {
+ "Text Files (*.txt)"
+ });
+ fd.setFilterExtensions(new String[] {
+ "*.txt"
+ });
+
+ String fName = fd.open();
+ if (fName != null) {
+ mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */
+ }
+
+ return fName;
+ }
+
+ private List<LogCatMessage> getSelectedLogCatMessages() {
+ int[] indices = mTable.getSelectionIndices();
+ Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */
+
+ List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length);
+ for (int i : indices) {
+ Object data = mTable.getItem(i).getData();
+ if (data instanceof LogCatMessage) {
+ selectedMessages.add((LogCatMessage) data);
+ }
+ }
+
+ return selectedMessages;
+ }
+
+ private List<LogCatMessage> applyCurrentFilters(List<LogCatMessage> msgList) {
+ List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(msgList.size());
+
+ for (LogCatMessage msg: msgList) {
+ if (isMessageAccepted(msg, mCurrentFilters)) {
+ filteredItems.add(msg);
+ }
+ }
+
+ return filteredItems;
+ }
+
+ private boolean isMessageAccepted(LogCatMessage msg, List<LogCatFilter> filters) {
+ for (LogCatFilter f : filters) {
+ if (!f.matches(msg)) {
+ // not accepted by this filter
+ return false;
+ }
+ }
+
+ // accepted by all filters
+ return true;
+ }
+
+ private void createLogcatViewTable(Composite parent) {
+ mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI);
+
+ mTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mTable.getHorizontalBar().setVisible(true);
+
+ /** Columns to show in the table. */
+ String[] properties = {
+ "Level",
+ "Time",
+ "PID",
+ "TID",
+ "Application",
+ "Tag",
+ "Text",
+ };
+
+ /** The sampleText for each column is used to determine the default widths
+ * for each column. The contents do not matter, only their lengths are needed. */
+ String[] sampleText = {
+ " ",
+ " 00-00 00:00:00.0000 ",
+ " 0000",
+ " 0000",
+ " com.android.launcher",
+ " SampleTagText",
+ " Log Message field should be pretty long by default. As long as possible for correct display on Mac.",
+ };
+
+ for (int i = 0; i < properties.length; i++) {
+ TableHelper.createTableColumn(mTable,
+ properties[i], /* Column title */
+ SWT.LEFT, /* Column Style */
+ sampleText[i], /* String to compute default col width */
+ getColPreferenceKey(properties[i]), /* Preference Store key for this column */
+ mPrefStore);
+ }
+
+ // don't zebra stripe the table: When the buffer is full, and scroll lock is on, having
+ // zebra striping means that the background could keep changing depending on the number
+ // of new messages added to the bottom of the log.
+ mTable.setLinesVisible(false);
+ mTable.setHeaderVisible(true);
+
+ // Set the row height to be sufficient enough to display the current font.
+ // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So
+ // we explicitly set it to be sure.
+ mTable.addListener(SWT.MeasureItem, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ event.height = event.gc.getFontMetrics().getHeight();
+ }
+ });
+
+ // Update the label provider whenever the text column's width changes
+ TableColumn textColumn = mTable.getColumn(properties.length - 1);
+ textColumn.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent event) {
+ recomputeWrapWidth();
+ }
+ });
+
+ addRightClickMenu(mTable);
+ initDoubleClickListener();
+ recomputeWrapWidth();
+
+ mTable.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent arg0) {
+ dispose();
+ }
+ });
+
+ final ScrollBar vbar = mTable.getVerticalBar();
+ mScrollBarSelectionListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (!mAutoScrollLock) {
+ return;
+ }
+
+ // thumb + selection < max => bar is not at the bottom.
+ // We subtract an arbitrary amount (thumbSize/2) from this difference to allow
+ // for cases like half a line being displayed at the end from affecting this
+ // calculation. The thumbSize/2 number seems to work experimentally across
+ // Linux/Mac & Windows, but might possibly need tweaking.
+ int diff = vbar.getThumb() + vbar.getSelection() - vbar.getMaximum();
+ boolean isAtBottom = Math.abs(diff) < vbar.getThumb() / 2;
+
+ if (isAtBottom != mShouldScrollToLatestLog) {
+ setScrollToLatestLog(isAtBottom);
+ mScrollLockCheckBox.setSelection(isAtBottom);
+ }
+ }
+ };
+ startScrollBarMonitor(vbar);
+
+ // Explicitly set the values to use for the scroll bar. In particular, we want these values
+ // to have a high enough accuracy that even small movements of the scroll bar have an
+ // effect on the selection. The auto scroll lock detection assumes that the scroll bar is
+ // at the bottom iff selection + thumb == max.
+ final int MAX = 10000;
+ final int THUMB = 10;
+ vbar.setValues(MAX - THUMB, // selection
+ 0, // min
+ MAX, // max
+ THUMB, // thumb
+ 1, // increment
+ THUMB); // page increment
+ }
+
+ private void startScrollBarMonitor(ScrollBar vbar) {
+ synchronized (mScrollBarSelectionListenerLock) {
+ if (!mScrollBarListenerSet) {
+ mScrollBarListenerSet = true;
+ vbar.addSelectionListener(mScrollBarSelectionListener);
+ }
+ }
+ }
+
+ private void stopScrollBarMonitor(ScrollBar vbar) {
+ synchronized (mScrollBarSelectionListenerLock) {
+ if (mScrollBarListenerSet) {
+ mScrollBarListenerSet = false;
+ vbar.removeSelectionListener(mScrollBarSelectionListener);
+ }
+ }
+ }
+
+ /** Setup menu to be displayed when right clicking a log message. */
+ private void addRightClickMenu(final Table table) {
+ // This action will pop up a create filter dialog pre-populated with current selection
+ final Action filterAction = new Action("Filter similar messages...") {
+ @Override
+ public void run() {
+ List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+ if (selectedMessages.size() == 0) {
+ addNewFilter();
+ } else {
+ LogCatMessage m = selectedMessages.get(0);
+ addNewFilter(m.getTag(), m.getMessage(), m.getPid(), m.getAppName(),
+ m.getLogLevel());
+ }
+ }
+ };
+
+ final Action findAction = new Action("Find...") {
+ @Override
+ public void run() {
+ showFindDialog();
+ };
+ };
+
+ final MenuManager mgr = new MenuManager();
+ mgr.add(filterAction);
+ mgr.add(findAction);
+ final Menu menu = mgr.createContextMenu(table);
+
+ table.addListener(SWT.MenuDetect, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y));
+ Rectangle clientArea = table.getClientArea();
+
+ // The click location is in the header if it is between
+ // clientArea.y and clientArea.y + header height
+ boolean header = pt.y > clientArea.y
+ && pt.y < (clientArea.y + table.getHeaderHeight());
+
+ // Show the menu only if it is not inside the header
+ table.setMenu(header ? null : menu);
+ }
+ });
+ }
+
+ public void recomputeWrapWidth() {
+ if (mTable == null || mTable.isDisposed()) {
+ return;
+ }
+
+ // get width of the last column (log message)
+ TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1);
+ int colWidth = tc.getWidth();
+
+ // get font width
+ GC gc = new GC(tc.getParent());
+ gc.setFont(mFont);
+ int avgCharWidth = gc.getFontMetrics().getAverageCharWidth();
+ gc.dispose();
+
+ int MIN_CHARS_PER_LINE = 50; // show atleast these many chars per line
+ mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE);
+
+ int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line
+ mWrapWidthInChars -= OFFSET_AT_END_OF_LINE;
+ }
+
+ private void setScrollToLatestLog(boolean scroll) {
+ mShouldScrollToLatestLog = scroll;
+ if (scroll) {
+ scrollToLatestLog();
+ }
+ }
+
+ private String getColPreferenceKey(String field) {
+ return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field;
+ }
+
+ private Font getFontFromPrefStore() {
+ FontData fd = PreferenceConverter.getFontData(mPrefStore,
+ LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY);
+ return new Font(Display.getDefault(), fd);
+ }
+
+ private Color getColorFromPrefStore(String key) {
+ RGB rgb = PreferenceConverter.getColor(mPrefStore, key);
+ return new Color(Display.getDefault(), rgb);
+ }
+
+ private void setupDefaults() {
+ int defaultFilterIndex = 0;
+ mFiltersTableViewer.getTable().setSelection(defaultFilterIndex);
+
+ filterSelectionChanged();
+ }
+
+ /**
+ * Perform all necessary updates whenever a filter is selected (by user or programmatically).
+ */
+ private void filterSelectionChanged() {
+ int idx = mFiltersTableViewer.getTable().getSelectionIndex();
+ if (idx == -1) {
+ /* One of the filters should always be selected.
+ * On Linux, there is no way to deselect an item.
+ * On Mac, clicking inside the list view, but not an any item will result
+ * in all items being deselected. In such a case, we simply reselect the
+ * first entry. */
+ idx = 0;
+ mFiltersTableViewer.getTable().setSelection(idx);
+ }
+
+ mCurrentSelectedFilterIndex = idx;
+
+ resetUnreadCountForAllFilters();
+ updateFiltersToolBar();
+ updateAppliedFilters();
+ }
+
+ private void resetUnreadCountForAllFilters() {
+ for (LogCatFilterData fd: mLogCatFilterData.values()) {
+ fd.resetUnreadCount();
+ }
+ refreshFiltersTable();
+ }
+
+ private void updateFiltersToolBar() {
+ /* The default filter at index 0 can neither be edited, nor removed. */
+ boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX;
+ mEditFilterToolItem.setEnabled(en);
+ mDeleteFilterToolItem.setEnabled(en);
+ }
+
+ private void updateAppliedFilters() {
+ mCurrentFilters = getFiltersToApply();
+ reloadLogBuffer();
+ }
+
+ private List<LogCatFilter> getFiltersToApply() {
+ /* list of filters to apply = saved filter + live filters */
+ List<LogCatFilter> filters = new ArrayList<LogCatFilter>();
+
+ if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) {
+ filters.add(getSelectedSavedFilter());
+ }
+
+ filters.addAll(getCurrentLiveFilters());
+ return filters;
+ }
+
+ private List<LogCatFilter> getCurrentLiveFilters() {
+ return LogCatFilter.fromString(
+ mLiveFilterText.getText(), /* current query */
+ LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */
+ }
+
+ private LogCatFilter getSelectedSavedFilter() {
+ return mLogCatFilters.get(mCurrentSelectedFilterIndex);
+ }
+
+ @Override
+ public void setFocus() {
+ }
+
+ @Override
+ public void bufferChanged(List<LogCatMessage> addedMessages,
+ List<LogCatMessage> deletedMessages) {
+ updateUnreadCount(addedMessages);
+ refreshFiltersTable();
+
+ synchronized (mLogBuffer) {
+ addedMessages = applyCurrentFilters(addedMessages);
+ deletedMessages = applyCurrentFilters(deletedMessages);
+
+ mLogBuffer.addAll(addedMessages);
+ mDeletedLogCount += deletedMessages.size();
+ }
+
+ refreshLogCatTable();
+ }
+
+ private void reloadLogBuffer() {
+ mTable.removeAll();
+
+ synchronized (mLogBuffer) {
+ mLogBuffer.clear();
+ mDeletedLogCount = 0;
+ }
+
+ if (mReceiver == null || mReceiver.getMessages() == null) {
+ return;
+ }
+
+ List<LogCatMessage> addedMessages = mReceiver.getMessages().getAllMessages();
+ List<LogCatMessage> deletedMessages = Collections.emptyList();
+ bufferChanged(addedMessages, deletedMessages);
+ }
+
+ /**
+ * When new messages are received, and they match a saved filter, update
+ * the unread count associated with that filter.
+ * @param receivedMessages list of new messages received
+ */
+ private void updateUnreadCount(List<LogCatMessage> receivedMessages) {
+ for (int i = 0; i < mLogCatFilters.size(); i++) {
+ if (i == mCurrentSelectedFilterIndex) {
+ /* no need to update unread count for currently selected filter */
+ continue;
+ }
+ LogCatFilter f = mLogCatFilters.get(i);
+ LogCatFilterData fd = mLogCatFilterData.get(f);
+ fd.updateUnreadCount(receivedMessages);
+ }
+ }
+
+ private void refreshFiltersTable() {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mFiltersTableViewer.getTable().isDisposed()) {
+ return;
+ }
+ mFiltersTableViewer.refresh();
+ }
+ });
+ }
+
+ /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */
+ private LogCatTableRefresherTask mCurrentRefresher;
+
+ /**
+ * Refresh the logcat table asynchronously from the UI thread.
+ * This method adds a new async refresh only if there are no pending refreshes for the table.
+ * Doing so eliminates redundant refresh threads from being queued up to be run on the
+ * display thread.
+ */
+ private void refreshLogCatTable() {
+ synchronized (this) {
+ if (mCurrentRefresher == null) {
+ mCurrentRefresher = new LogCatTableRefresherTask();
+ Display.getDefault().asyncExec(mCurrentRefresher);
+ }
+ }
+ }
+
+ /**
+ * The {@link LogCatTableRefresherTask} takes care of refreshing the table with the
+ * new log messages that have been received. Since the log behaves like a circular buffer,
+ * the first step is to remove items from the top of the table (if necessary). This step
+ * is complicated by the fact that a single log message may span multiple rows if the message
+ * was wrapped. Once the deleted items are removed, the new messages are added to the bottom
+ * of the table. If scroll lock is enabled, the item that was original visible is made visible
+ * again, if not, the last item is made visible.
+ */
+ private class LogCatTableRefresherTask implements Runnable {
+ @Override
+ public void run() {
+ if (mTable.isDisposed()) {
+ return;
+ }
+ synchronized (LogCatPanel.this) {
+ mCurrentRefresher = null;
+ }
+
+ // Current topIndex so that it can be restored if scroll locked.
+ int topIndex = mTable.getTopIndex();
+
+ mTable.setRedraw(false);
+
+ // the scroll bar should only listen to user generated scroll events, not the
+ // scroll events that happen due to the addition of logs
+ stopScrollBarMonitor(mTable.getVerticalBar());
+
+ // Obtain the list of new messages, and the number of deleted messages.
+ List<LogCatMessage> newMessages;
+ int deletedMessageCount;
+ synchronized (mLogBuffer) {
+ newMessages = new ArrayList<LogCatMessage>(mLogBuffer);
+ mLogBuffer.clear();
+
+ deletedMessageCount = mDeletedLogCount;
+ mDeletedLogCount = 0;
+
+ mFindTarget.scrollBy(deletedMessageCount);
+ }
+
+ int originalItemCount = mTable.getItemCount();
+
+ // Remove entries from the start of the table if they were removed in the log buffer
+ // This is complicated by the fact that a single message may span multiple TableItems
+ // if it was word-wrapped.
+ deletedMessageCount -= removeFromTable(mTable, deletedMessageCount);
+
+ // Compute number of table items that were deleted from the table.
+ int deletedItemCount = originalItemCount - mTable.getItemCount();
+
+ // If there are more messages to delete (after deleting messages from the table),
+ // then delete them from the start of the newly added messages list
+ if (deletedMessageCount > 0) {
+ assert deletedMessageCount < newMessages.size();
+ for (int i = 0; i < deletedMessageCount; i++) {
+ newMessages.remove(0);
+ }
+ }
+
+ // Add the remaining messages to the table.
+ for (LogCatMessage m: newMessages) {
+ List<String> wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars);
+ Color c = getForegroundColor(m);
+ for (int i = 0; i < wrappedMessageList.size(); i++) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+
+ if (i == 0) {
+ // Only set the message data in the first item. This allows code that
+ // examines the table item data (such as copy selection) to distinguish
+ // between real messages versus lines that are really just wrapped
+ // content from the previous message.
+ item.setData(m);
+
+ item.setText(new String[] {
+ Character.toString(m.getLogLevel().getPriorityLetter()),
+ m.getTime(),
+ m.getPid(),
+ m.getTid(),
+ m.getAppName(),
+ m.getTag(),
+ wrappedMessageList.get(i)
+ });
+ } else {
+ item.setText(new String[] {
+ "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ wrappedMessageList.get(i)
+ });
+ }
+ item.setForeground(c);
+ item.setFont(mFont);
+ }
+ }
+
+ if (mShouldScrollToLatestLog) {
+ scrollToLatestLog();
+ } else {
+ // If scroll locked, show the same item that was original visible in the table.
+ int index = Math.max(topIndex - deletedItemCount, 0);
+ mTable.setTopIndex(index);
+ }
+
+ mTable.setRedraw(true);
+
+ // re-enable listening to scroll bar events, but do so in a separate thread to make
+ // sure that the current task (LogCatRefresherTask) has completed first
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mTable.isDisposed()) {
+ startScrollBarMonitor(mTable.getVerticalBar());
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes given number of messages from the table, starting at the top of the table.
+ * Note that the number of messages deleted is not equal to the number of rows
+ * deleted since a single message could span multiple rows. This method first calculates
+ * the number of rows that correspond to the number of messages to delete, and then
+ * removes all those rows.
+ * @param table table from which messages should be removed
+ * @param msgCount number of messages to be removed
+ * @return number of messages that were actually removed
+ */
+ private int removeFromTable(Table table, int msgCount) {
+ int deletedMessageCount = 0; // # of messages that have been deleted
+ int lastItemToDelete = 0; // index of the last item that should be deleted
+
+ while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) {
+ // only rows that begin a message have their item data set
+ TableItem item = table.getItem(lastItemToDelete);
+ if (item.getData() != null) {
+ deletedMessageCount++;
+ }
+
+ lastItemToDelete++;
+ }
+
+ // If there are any table items left over at the end that are wrapped over from the
+ // previous message, mark them for deletion as well.
+ if (lastItemToDelete < table.getItemCount()
+ && table.getItem(lastItemToDelete).getData() == null) {
+ lastItemToDelete++;
+ }
+
+ table.remove(0, lastItemToDelete - 1);
+
+ return deletedMessageCount;
+ }
+ }
+
+ /** Scroll to the last line. */
+ private void scrollToLatestLog() {
+ if (!mTable.isDisposed()) {
+ mTable.setTopIndex(mTable.getItemCount() - 1);
+ }
+ }
+
+ /**
+ * Splits the message into multiple lines if the message length exceeds given width.
+ * If the message was split, then a wrap character \u23ce is appended to the end of all
+ * lines but the last one.
+ */
+ private List<String> wrapMessage(String msg, int wrapWidth) {
+ if (msg.length() < wrapWidth) {
+ return Collections.singletonList(msg);
+ }
+
+ List<String> wrappedMessages = new ArrayList<String>();
+
+ int offset = 0;
+ int len = msg.length();
+
+ while (len > 0) {
+ int copylen = Math.min(wrapWidth, len);
+ String s = msg.substring(offset, offset + copylen);
+
+ offset += copylen;
+ len -= copylen;
+
+ if (len > 0) { // if there are more lines following, then append a wrap marker
+ s += " \u23ce"; //$NON-NLS-1$
+ }
+
+ wrappedMessages.add(s);
+ }
+
+ return wrappedMessages;
+ }
+
+ private Color getForegroundColor(LogCatMessage m) {
+ LogLevel l = m.getLogLevel();
+
+ if (l.equals(LogLevel.VERBOSE)) {
+ return mVerboseColor;
+ } else if (l.equals(LogLevel.INFO)) {
+ return mInfoColor;
+ } else if (l.equals(LogLevel.DEBUG)) {
+ return mDebugColor;
+ } else if (l.equals(LogLevel.ERROR)) {
+ return mErrorColor;
+ } else if (l.equals(LogLevel.WARN)) {
+ return mWarnColor;
+ } else if (l.equals(LogLevel.ASSERT)) {
+ return mAssertColor;
+ }
+
+ return mVerboseColor;
+ }
+
+ private List<ILogCatMessageSelectionListener> mMessageSelectionListeners;
+
+ private void initDoubleClickListener() {
+ mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1);
+
+ mTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent arg0) {
+ List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+ if (selectedMessages.size() == 0) {
+ return;
+ }
+
+ for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) {
+ l.messageDoubleClicked(selectedMessages.get(0));
+ }
+ }
+ });
+ }
+
+ public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) {
+ mMessageSelectionListeners.add(l);
+ }
+
+ private ITableFocusListener mTableFocusListener;
+
+ /**
+ * Specify the listener to be called when the logcat view gets focus. This interface is
+ * required by DDMS to hook up the menu items for Copy and Select All.
+ * @param listener listener to be notified when logcat view is in focus
+ */
+ public void setTableFocusListener(ITableFocusListener listener) {
+ mTableFocusListener = listener;
+
+ final IFocusedTableActivator activator = new IFocusedTableActivator() {
+ @Override
+ public void copy(Clipboard clipboard) {
+ copySelectionToClipboard(clipboard);
+ }
+
+ @Override
+ public void selectAll() {
+ mTable.selectAll();
+ }
+ };
+
+ mTable.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ mTableFocusListener.focusGained(activator);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mTableFocusListener.focusLost(activator);
+ }
+ });
+ }
+
+ /** Copy all selected messages to clipboard. */
+ public void copySelectionToClipboard(Clipboard clipboard) {
+ StringBuilder sb = new StringBuilder();
+
+ for (LogCatMessage m : getSelectedLogCatMessages()) {
+ sb.append(m.toString());
+ sb.append('\n');
+ }
+
+ if (sb.length() > 0) {
+ clipboard.setContents(
+ new Object[] {sb.toString()},
+ new Transfer[] {TextTransfer.getInstance()}
+ );
+ }
+ }
+
+ /** Select all items in the logcat table. */
+ public void selectAll() {
+ mTable.selectAll();
+ }
+
+ private void dispose() {
+ if (mFont != null && !mFont.isDisposed()) {
+ mFont.dispose();
+ }
+
+ if (mVerboseColor != null && !mVerboseColor.isDisposed()) {
+ disposeMessageColors();
+ }
+ }
+
+ private void disposeMessageColors() {
+ mVerboseColor.dispose();
+ mDebugColor.dispose();
+ mInfoColor.dispose();
+ mWarnColor.dispose();
+ mErrorColor.dispose();
+ mAssertColor.dispose();
+ }
+
+ private class LogcatFindTarget extends AbstractBufferFindTarget {
+ @Override
+ public void selectAndReveal(int index) {
+ mTable.deselectAll();
+ mTable.select(index);
+ mTable.showSelection();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mTable.getItemCount();
+ }
+
+ @Override
+ public String getItem(int index) {
+ Object data = mTable.getItem(index).getData();
+ if (data != null) {
+ return data.toString();
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getStartingIndex() {
+ // start searches from current selection if present, otherwise from the tail end
+ // of the buffer
+ int s = mTable.getSelectionIndex();
+ if (s != -1) {
+ return s;
+ } else {
+ return getItemCount() - 1;
+ }
+ };
+ };
+
+ private FindDialog mFindDialog;
+ private LogcatFindTarget mFindTarget = new LogcatFindTarget();
+ public void showFindDialog() {
+ if (mFindDialog != null) {
+ // if the dialog is already displayed
+ return;
+ }
+
+ mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget);
+ mFindDialog.open(); // blocks until find dialog is closed
+ mFindDialog = null;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java
new file mode 100644
index 0000000..a85cd03
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatListener;
+import com.android.ddmlib.logcat.LogCatMessage;
+import com.android.ddmlib.logcat.LogCatReceiverTask;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A class to monitor a device for logcat messages. It stores the received
+ * log messages in a circular buffer.
+ */
+public final class LogCatReceiver implements LogCatListener {
+ private static LogCatMessage DEVICE_DISCONNECTED_MESSAGE =
+ new LogCatMessage(LogLevel.ERROR, "", "", "",
+ "", "", "Device disconnected");
+
+ private LogCatMessageList mLogMessages;
+ private IDevice mCurrentDevice;
+ private LogCatReceiverTask mLogCatReceiverTask;
+ private Set<ILogCatBufferChangeListener> mLogCatMessageListeners;
+ private IPreferenceStore mPrefStore;
+
+ /**
+ * Construct a LogCat message receiver for provided device. This will launch a
+ * logcat command on the device, and monitor the output of that command in
+ * a separate thread. All logcat messages are then stored in a circular
+ * buffer, which can be retrieved using {@link LogCatReceiver#getMessages()}.
+ * @param device device to monitor for logcat messages
+ * @param prefStore
+ */
+ public LogCatReceiver(IDevice device, IPreferenceStore prefStore) {
+ mCurrentDevice = device;
+ mPrefStore = prefStore;
+
+ mLogCatMessageListeners = new HashSet<ILogCatBufferChangeListener>();
+ mLogMessages = new LogCatMessageList(getFifoSize());
+
+ startReceiverThread();
+ }
+
+ /**
+ * Stop receiving messages from currently active device.
+ */
+ public void stop() {
+ if (mLogCatReceiverTask != null) {
+ /* stop the current logcat command */
+ mLogCatReceiverTask.removeLogCatListener(this);
+ mLogCatReceiverTask.stop();
+ mLogCatReceiverTask = null;
+
+ // add a message to the log indicating that the device has been disconnected.
+ log(Collections.singletonList(DEVICE_DISCONNECTED_MESSAGE));
+ }
+
+ mCurrentDevice = null;
+ }
+
+ private int getFifoSize() {
+ int n = mPrefStore.getInt(LogCatMessageList.MAX_MESSAGES_PREFKEY);
+ return n == 0 ? LogCatMessageList.MAX_MESSAGES_DEFAULT : n;
+ }
+
+ private void startReceiverThread() {
+ if (mCurrentDevice == null) {
+ return;
+ }
+
+ mLogCatReceiverTask = new LogCatReceiverTask(mCurrentDevice);
+ mLogCatReceiverTask.addLogCatListener(this);
+
+ Thread t = new Thread(mLogCatReceiverTask);
+ t.setName("LogCat output receiver for " + mCurrentDevice.getSerialNumber());
+ t.start();
+ }
+
+ @Override
+ public void log(List<LogCatMessage> newMessages) {
+ List<LogCatMessage> deletedMessages;
+ synchronized (mLogMessages) {
+ deletedMessages = mLogMessages.ensureSpace(newMessages.size());
+ mLogMessages.appendMessages(newMessages);
+ }
+ sendLogChangedEvent(newMessages, deletedMessages);
+ }
+
+ /**
+ * Get the list of logcat messages received from currently active device.
+ * @return list of messages if currently listening, null otherwise
+ */
+ public LogCatMessageList getMessages() {
+ return mLogMessages;
+ }
+
+ /**
+ * Clear the list of messages received from the currently active device.
+ */
+ public void clearMessages() {
+ mLogMessages.clear();
+ }
+
+ /**
+ * Add to list of message event listeners.
+ * @param l listener to notified when messages are received from the device
+ */
+ public void addMessageReceivedEventListener(ILogCatBufferChangeListener l) {
+ mLogCatMessageListeners.add(l);
+ }
+
+ public void removeMessageReceivedEventListener(ILogCatBufferChangeListener l) {
+ mLogCatMessageListeners.remove(l);
+ }
+
+ private void sendLogChangedEvent(List<LogCatMessage> addedMessages,
+ List<LogCatMessage> deletedMessages) {
+ for (ILogCatBufferChangeListener l : mLogCatMessageListeners) {
+ l.bufferChanged(addedMessages, deletedMessages);
+ }
+ }
+
+ /**
+ * Resize the internal FIFO.
+ * @param size new size
+ */
+ public void resizeFifo(int size) {
+ mLogMessages.resize(size);
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java
new file mode 100644
index 0000000..5b25e17
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A factory for {@link LogCatReceiver} objects. Its primary objective is to cache
+ * constructed {@link LogCatReceiver}'s per device and hand them back when requested.
+ */
+public class LogCatReceiverFactory {
+ /** Singleton instance. */
+ public static final LogCatReceiverFactory INSTANCE = new LogCatReceiverFactory();
+
+ private Map<String, LogCatReceiver> mReceiverCache = new HashMap<String, LogCatReceiver>();
+
+ /** Private constructor: cannot instantiate. */
+ private LogCatReceiverFactory() {
+ AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() {
+ @Override
+ public void deviceDisconnected(final IDevice device) {
+ // The deviceDisconnected() is called from DDMS code that holds
+ // multiple locks regarding list of clients, etc.
+ // It so happens that #newReceiver() below adds a clientChangeListener
+ // which requires those locks as well. So if we call
+ // #removeReceiverFor from a DDMS/Monitor thread, we could end up
+ // in a deadlock. As a result, we spawn a separate thread that
+ // doesn't hold any of the DDMS locks to remove the receiver.
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ removeReceiverFor(device); }
+ }, "Remove logcat receiver for " + device.getSerialNumber());
+ t.start();
+ }
+
+ @Override
+ public void deviceConnected(IDevice device) {
+ }
+
+ @Override
+ public void deviceChanged(IDevice device, int changeMask) {
+ }
+ });
+ }
+
+ /**
+ * Remove existing logcat receivers. This method should not be called from a DDMS thread
+ * context that might be holding locks. Doing so could result in a deadlock with the following
+ * two threads locked up: <ul>
+ * <li> {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory},
+ * while holding a DDMS monitor internal lock. </li>
+ * <li> {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory}
+ * while attempting to obtain a DDMS monitor lock. </li>
+ * </ul>
+ */
+ private synchronized void removeReceiverFor(IDevice device) {
+ LogCatReceiver r = mReceiverCache.get(device.getSerialNumber());
+ if (r != null) {
+ r.stop();
+ mReceiverCache.remove(device.getSerialNumber());
+ }
+ }
+
+ public synchronized LogCatReceiver newReceiver(IDevice device, IPreferenceStore prefs) {
+ LogCatReceiver r = mReceiverCache.get(device.getSerialNumber());
+ if (r != null) {
+ return r;
+ }
+
+ r = new LogCatReceiver(device, prefs);
+ mReceiverCache.put(device.getSerialNumber(), r);
+ return r;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
new file mode 100644
index 0000000..3da9fd0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class that can determine if a string matches the exception
+ * stack trace pattern, and if so, can provide the java source file
+ * and line where the exception occured.
+ */
+public final class LogCatStackTraceParser {
+ /** Regex to match a stack trace line. E.g.:
+ * at com.foo.Class.method(FileName.extension:10)
+ * extension is typically java, but can be anything (java/groovy/scala/..).
+ */
+ private static final String EXCEPTION_LINE_REGEX =
+ "\\s*at\\ (.*)\\((.*)\\..*\\:(\\d+)\\)"; //$NON-NLS-1$
+
+ private static final Pattern EXCEPTION_LINE_PATTERN =
+ Pattern.compile(EXCEPTION_LINE_REGEX);
+
+ /**
+ * Identify if a input line matches the expected pattern
+ * for a stack trace from an exception.
+ */
+ public boolean isValidExceptionTrace(String line) {
+ return EXCEPTION_LINE_PATTERN.matcher(line).find();
+ }
+
+ /**
+ * Get fully qualified method name that threw the exception.
+ * @param line line from the stack trace, must have been validated with
+ * {@link LogCatStackTraceParser#isValidExceptionTrace(String)} before calling this method.
+ * @return fully qualified method name
+ */
+ public String getMethodName(String line) {
+ Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+ m.find();
+ return m.group(1);
+ }
+
+ /**
+ * Get source file name where exception was generated. Input line must be first validated with
+ * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+ */
+ public String getFileName(String line) {
+ Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+ m.find();
+ return m.group(2);
+ }
+
+ /**
+ * Get line number where exception was generated. Input line must be first validated with
+ * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+ */
+ public int getLineNumber(String line) {
+ Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+ m.find();
+ try {
+ return Integer.parseInt(m.group(3));
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java
new file mode 100644
index 0000000..9cff656
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import org.eclipse.swt.graphics.Color;
+
+public class LogColors {
+ public Color infoColor;
+ public Color debugColor;
+ public Color errorColor;
+ public Color warningColor;
+ public Color verboseColor;
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java
new file mode 100644
index 0000000..74a5e37
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.logcat.LogPanel.LogMessage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.regex.PatternSyntaxException;
+
+/** logcat output filter class */
+public class LogFilter {
+
+ public final static int MODE_PID = 0x01;
+ public final static int MODE_TAG = 0x02;
+ public final static int MODE_LEVEL = 0x04;
+
+ private String mName;
+
+ /**
+ * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL
+ */
+ private int mMode = 0;
+
+ /**
+ * pid used for filtering. Only valid if mMode is MODE_PID.
+ */
+ private int mPid;
+
+ /** Single level log level as defined in Log.mLevelChar. Only valid
+ * if mMode is MODE_LEVEL */
+ private int mLogLevel;
+
+ /**
+ * log tag filtering. Only valid if mMode is MODE_TAG
+ */
+ private String mTag;
+
+ private Table mTable;
+ private TabItem mTabItem;
+ private boolean mIsCurrentTabItem = false;
+ private int mUnreadCount = 0;
+
+ /** Temp keyword filtering */
+ private String[] mTempKeywordFilters;
+
+ /** temp pid filtering */
+ private int mTempPid = -1;
+
+ /** temp tag filtering */
+ private String mTempTag;
+
+ /** temp log level filtering */
+ private int mTempLogLevel = -1;
+
+ private LogColors mColors;
+
+ private boolean mTempFilteringStatus = false;
+
+ private final ArrayList<LogMessage> mMessages = new ArrayList<LogMessage>();
+ private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>();
+
+ private boolean mSupportsDelete = true;
+ private boolean mSupportsEdit = true;
+ private int mRemovedMessageCount = 0;
+
+ /**
+ * Creates a filter with a particular mode.
+ * @param name The name to be displayed in the UI
+ */
+ public LogFilter(String name) {
+ mName = name;
+ }
+
+ public LogFilter() {
+
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(mName);
+
+ sb.append(':');
+ sb.append(mMode);
+ if ((mMode & MODE_PID) == MODE_PID) {
+ sb.append(':');
+ sb.append(mPid);
+ }
+
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ sb.append(':');
+ sb.append(mLogLevel);
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG) {
+ sb.append(':');
+ sb.append(mTag);
+ }
+
+ return sb.toString();
+ }
+
+ public boolean loadFromString(String string) {
+ String[] segments = string.split(":"); //$NON-NLS-1$
+ int index = 0;
+
+ // get the name
+ mName = segments[index++];
+
+ // get the mode
+ mMode = Integer.parseInt(segments[index++]);
+
+ if ((mMode & MODE_PID) == MODE_PID) {
+ mPid = Integer.parseInt(segments[index++]);
+ }
+
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ mLogLevel = Integer.parseInt(segments[index++]);
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG) {
+ mTag = segments[index++];
+ }
+
+ return true;
+ }
+
+
+ /** Sets the name of the filter. */
+ void setName(String name) {
+ mName = name;
+ }
+
+ /**
+ * Returns the UI display name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Set the Table ui widget associated with this filter.
+ * @param tabItem The item in the TabFolder
+ * @param table The Table object
+ */
+ public void setWidgets(TabItem tabItem, Table table) {
+ mTable = table;
+ mTabItem = tabItem;
+ }
+
+ /**
+ * Returns true if the filter is ready for ui.
+ */
+ public boolean uiReady() {
+ return (mTable != null && mTabItem != null);
+ }
+
+ /**
+ * Returns the UI table object.
+ * @return
+ */
+ public Table getTable() {
+ return mTable;
+ }
+
+ public void dispose() {
+ mTable.dispose();
+ mTabItem.dispose();
+ mTable = null;
+ mTabItem = null;
+ }
+
+ /**
+ * Resets the filtering mode to be 0 (i.e. no filter).
+ */
+ public void resetFilteringMode() {
+ mMode = 0;
+ }
+
+ /**
+ * Returns the current filtering mode.
+ * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL
+ */
+ public int getFilteringMode() {
+ return mMode;
+ }
+
+ /**
+ * Adds PID to the current filtering mode.
+ * @param pid
+ */
+ public void setPidMode(int pid) {
+ if (pid != -1) {
+ mMode |= MODE_PID;
+ } else {
+ mMode &= ~MODE_PID;
+ }
+ mPid = pid;
+ }
+
+ /** Returns the pid filter if valid, otherwise -1 */
+ public int getPidFilter() {
+ if ((mMode & MODE_PID) == MODE_PID)
+ return mPid;
+ return -1;
+ }
+
+ public void setTagMode(String tag) {
+ if (tag != null && tag.length() > 0) {
+ mMode |= MODE_TAG;
+ } else {
+ mMode &= ~MODE_TAG;
+ }
+ mTag = tag;
+ }
+
+ public String getTagFilter() {
+ if ((mMode & MODE_TAG) == MODE_TAG)
+ return mTag;
+ return null;
+ }
+
+ public void setLogLevel(int level) {
+ if (level == -1) {
+ mMode &= ~MODE_LEVEL;
+ } else {
+ mMode |= MODE_LEVEL;
+ mLogLevel = level;
+ }
+
+ }
+
+ public int getLogLevel() {
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ return mLogLevel;
+ }
+
+ return -1;
+ }
+
+
+ public boolean supportsDelete() {
+ return mSupportsDelete ;
+ }
+
+ public boolean supportsEdit() {
+ return mSupportsEdit;
+ }
+
+ /**
+ * Sets the selected state of the filter.
+ * @param selected selection state.
+ */
+ public void setSelectedState(boolean selected) {
+ if (selected) {
+ if (mTabItem != null) {
+ mTabItem.setText(mName);
+ }
+ mUnreadCount = 0;
+ }
+ mIsCurrentTabItem = selected;
+ }
+
+ /**
+ * Adds a new message and optionally removes an old message.
+ * <p/>The new message is filtered through {@link #accept(LogMessage)}.
+ * Calls to {@link #flush()} from a UI thread will display it (and other
+ * pending messages) to the associated {@link Table}.
+ * @param logMessage the MessageData object to filter
+ * @return true if the message was accepted.
+ */
+ public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) {
+ synchronized (mMessages) {
+ if (oldMessage != null) {
+ int index = mMessages.indexOf(oldMessage);
+ if (index != -1) {
+ // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+ mMessages.remove(index);
+ mRemovedMessageCount++;
+ }
+
+ // now we look for it in mNewMessages. This can happen if the new message is added
+ // and then removed because too many messages are added between calls to #flush()
+ index = mNewMessages.indexOf(oldMessage);
+ if (index != -1) {
+ // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+ mNewMessages.remove(index);
+ }
+ }
+
+ boolean filter = accept(newMessage);
+
+ if (filter) {
+ // at this point the message is accepted, we add it to the list
+ mMessages.add(newMessage);
+ mNewMessages.add(newMessage);
+ }
+
+ return filter;
+ }
+ }
+
+ /**
+ * Removes all the items in the filter and its {@link Table}.
+ */
+ public void clear() {
+ mRemovedMessageCount = 0;
+ mNewMessages.clear();
+ mMessages.clear();
+ mTable.removeAll();
+ }
+
+ /**
+ * Filters a message.
+ * @param logMessage the Message
+ * @return true if the message is accepted by the filter.
+ */
+ boolean accept(LogMessage logMessage) {
+ // do the regular filtering now
+ if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) {
+ return false;
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG && (
+ logMessage.data.tag == null ||
+ logMessage.data.tag.equals(mTag) == false)) {
+ return false;
+ }
+
+ int msgLogLevel = logMessage.data.logLevel.getPriority();
+
+ // test the temp log filtering first, as it replaces the old one
+ if (mTempLogLevel != -1) {
+ if (mTempLogLevel > msgLogLevel) {
+ return false;
+ }
+ } else if ((mMode & MODE_LEVEL) == MODE_LEVEL &&
+ mLogLevel > msgLogLevel) {
+ return false;
+ }
+
+ // do the temp filtering now.
+ if (mTempKeywordFilters != null) {
+ String msg = logMessage.msg;
+
+ for (String kw : mTempKeywordFilters) {
+ try {
+ if (msg.contains(kw) == false && msg.matches(kw) == false) {
+ return false;
+ }
+ } catch (PatternSyntaxException e) {
+ // if the string is not a valid regular expression,
+ // this exception is thrown.
+ return false;
+ }
+ }
+ }
+
+ if (mTempPid != -1 && mTempPid != logMessage.data.pid) {
+ return false;
+ }
+
+ if (mTempTag != null && mTempTag.length() > 0) {
+ if (mTempTag.equals(logMessage.data.tag) == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Takes all the accepted messages and display them.
+ * This must be called from a UI thread.
+ */
+ @UiThread
+ public void flush() {
+ // if scroll bar is at the bottom, we will scroll
+ ScrollBar bar = mTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // if we are not going to scroll, get the current first item being shown.
+ int topIndex = mTable.getTopIndex();
+
+ // disable drawing
+ mTable.setRedraw(false);
+
+ int totalCount = mNewMessages.size();
+
+ try {
+ // remove the items of the old messages.
+ for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) {
+ mTable.remove(0);
+ }
+ mRemovedMessageCount = 0;
+
+ if (mUnreadCount > mTable.getItemCount()) {
+ mUnreadCount = mTable.getItemCount();
+ }
+
+ // add the new items
+ for (int i = 0 ; i < totalCount ; i++) {
+ LogMessage msg = mNewMessages.get(i);
+ addTableItem(msg);
+ }
+ } catch (SWTException e) {
+ // log the error and keep going. Content of the logcat table maybe unexpected
+ // but at least ddms won't crash.
+ Log.e("LogFilter", e);
+ }
+
+ // redraw
+ mTable.setRedraw(true);
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ totalCount = mTable.getItemCount();
+ if (totalCount > 0) {
+ mTable.showItem(mTable.getItem(totalCount-1));
+ }
+ } else if (mRemovedMessageCount > 0) {
+ // we need to make sure the topIndex is still visible.
+ // Because really old items are removed from the list, this could make it disappear
+ // if we don't change the scroll value at all.
+
+ topIndex -= mRemovedMessageCount;
+ if (topIndex < 0) {
+ // looks like it disappeared. Lets just show the first item
+ mTable.showItem(mTable.getItem(0));
+ } else {
+ mTable.showItem(mTable.getItem(topIndex));
+ }
+ }
+
+ // if this filter is not the current one, we update the tab text
+ // with the amount of unread message
+ if (mIsCurrentTabItem == false) {
+ mUnreadCount += mNewMessages.size();
+ totalCount = mTable.getItemCount();
+ if (mUnreadCount > 0) {
+ mTabItem.setText(mName + " (" //$NON-NLS-1$
+ + (mUnreadCount > totalCount ? totalCount : mUnreadCount)
+ + ")"); //$NON-NLS-1$
+ } else {
+ mTabItem.setText(mName); //$NON-NLS-1$
+ }
+ }
+
+ mNewMessages.clear();
+ }
+
+ void setColors(LogColors colors) {
+ mColors = colors;
+ }
+
+ int getUnreadCount() {
+ return mUnreadCount;
+ }
+
+ void setUnreadCount(int unreadCount) {
+ mUnreadCount = unreadCount;
+ }
+
+ void setSupportsDelete(boolean support) {
+ mSupportsDelete = support;
+ }
+
+ void setSupportsEdit(boolean support) {
+ mSupportsEdit = support;
+ }
+
+ void setTempKeywordFiltering(String[] segments) {
+ mTempKeywordFilters = segments;
+ mTempFilteringStatus = true;
+ }
+
+ void setTempPidFiltering(int pid) {
+ mTempPid = pid;
+ mTempFilteringStatus = true;
+ }
+
+ void setTempTagFiltering(String tag) {
+ mTempTag = tag;
+ mTempFilteringStatus = true;
+ }
+
+ void resetTempFiltering() {
+ if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) {
+ mTempFilteringStatus = true;
+ }
+
+ mTempPid = -1;
+ mTempTag = null;
+ mTempKeywordFilters = null;
+ }
+
+ void resetTempFilteringStatus() {
+ mTempFilteringStatus = false;
+ }
+
+ boolean getTempFilterStatus() {
+ return mTempFilteringStatus;
+ }
+
+
+ /**
+ * Add a TableItem for the index-th item of the buffer
+ * @param filter The index of the table in which to insert the item.
+ */
+ private void addTableItem(LogMessage msg) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(0, msg.data.time);
+ item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() }));
+ item.setText(2, msg.data.pidString);
+ item.setText(3, msg.data.tag);
+ item.setText(4, msg.msg);
+
+ // add the buffer index as data
+ item.setData(msg);
+
+ if (msg.data.logLevel == LogLevel.INFO) {
+ item.setForeground(mColors.infoColor);
+ } else if (msg.data.logLevel == LogLevel.DEBUG) {
+ item.setForeground(mColors.debugColor);
+ } else if (msg.data.logLevel == LogLevel.ERROR) {
+ item.setForeground(mColors.errorColor);
+ } else if (msg.data.logLevel == LogLevel.WARN) {
+ item.setForeground(mColors.warningColor);
+ } else {
+ item.setForeground(mColors.verboseColor);
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java
new file mode 100644
index 0000000..a347155
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java
@@ -0,0 +1,1626 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LogPanel extends SelectionDependentPanel {
+
+ private static final int STRING_BUFFER_LENGTH = 10000;
+
+ /** no filtering. Only one tab with everything. */
+ public static final int FILTER_NONE = 0;
+ /** manual mode for filter. all filters are manually created. */
+ public static final int FILTER_MANUAL = 1;
+ /** automatic mode for filter (pid mode).
+ * All filters are automatically created. */
+ public static final int FILTER_AUTO_PID = 2;
+ /** automatic mode for filter (tag mode).
+ * All filters are automatically created. */
+ public static final int FILTER_AUTO_TAG = 3;
+ /** Manual filtering mode + new filter for debug app, if needed */
+ public static final int FILTER_DEBUG = 4;
+
+ public static final int COLUMN_MODE_MANUAL = 0;
+ public static final int COLUMN_MODE_AUTO = 1;
+
+ public static String PREFS_TIME;
+ public static String PREFS_LEVEL;
+ public static String PREFS_PID;
+ public static String PREFS_TAG;
+ public static String PREFS_MESSAGE;
+
+ /**
+ * This pattern is meant to parse the first line of a log message with the option
+ * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
+ * following lines are the message (can be several line).<br>
+ * This first line looks something like<br>
+ * <code>"[ 00-00 00:00:00.000 &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 IDevice mCurrentLoggedDevice = null;
+
+ private ICommonAction mDeleteFilterAction;
+ private ICommonAction mEditFilterAction;
+
+ private ICommonAction[] mLogLevelActions;
+
+ /** message data, separated from content for multi line messages */
+ protected static class LogMessageInfo {
+ public LogLevel logLevel;
+ public int pid;
+ public String pidString;
+ public String tag;
+ public String time;
+ }
+
+ /** pointer to the latest LogMessageInfo. this is used for multi line
+ * log message, to reuse the info regarding level, pid, etc...
+ */
+ private LogMessageInfo mLastMessageInfo = null;
+
+ private boolean mPendingAsyncRefresh = false;
+
+ private String mDefaultLogSave;
+
+ private int mColumnMode = COLUMN_MODE_MANUAL;
+ private Font mDisplayFont;
+
+ private ITableFocusListener mGlobalListener;
+
+ private LogCatViewInterface mLogCatViewInterface = null;
+
+ /** message data, separated from content for multi line messages */
+ protected static class LogMessage {
+ public LogMessageInfo data;
+ public String msg;
+
+ @Override
+ public String toString() {
+ return data.time + ": " //$NON-NLS-1$
+ + data.logLevel + "/" //$NON-NLS-1$
+ + data.tag + "(" //$NON-NLS-1$
+ + data.pidString + "): " //$NON-NLS-1$
+ + msg;
+ }
+ }
+
+ /**
+ * objects able to receive the output of a remote shell command,
+ * specifically a logcat command in this case
+ */
+ private final class LogCatOuputReceiver extends MultiLineReceiver {
+
+ public boolean isCancelled = false;
+
+ public LogCatOuputReceiver() {
+ super();
+
+ setTrimLine(false);
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ if (isCancelled == false) {
+ processLogLines(lines);
+ }
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return isCancelled;
+ }
+ }
+
+ /**
+ * Parser class for the output of a "ps" shell command executed on a device.
+ * This class looks for a specific pid to find the process name from it.
+ * Once found, the name is used to update a filter and a tab object
+ *
+ */
+ private class PsOutputReceiver extends MultiLineReceiver {
+
+ private LogFilter mFilter;
+
+ private TabItem mTabItem;
+
+ private int mPid;
+
+ /** set to true when we've found the pid we're looking for */
+ private boolean mDone = false;
+
+ PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) {
+ mPid = pid;
+ mFilter = filter;
+ mTabItem = tabItem;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return mDone;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.startsWith("USER")) { //$NON-NLS-1$
+ continue;
+ }
+ // get the pid.
+ int index = line.indexOf(' ');
+ if (index == -1) {
+ continue;
+ }
+ // look for the next non blank char
+ index++;
+ while (line.charAt(index) == ' ') {
+ index++;
+ }
+
+ // this is the start of the pid.
+ // look for the end.
+ int index2 = line.indexOf(' ', index);
+
+ // get the line
+ String pidStr = line.substring(index, index2);
+ int pid = Integer.parseInt(pidStr);
+ if (pid != mPid) {
+ continue;
+ } else {
+ // get the process name
+ index = line.lastIndexOf(' ');
+ final String name = line.substring(index + 1);
+
+ mFilter.setName(name);
+
+ // update the tab
+ Display d = mFolders.getDisplay();
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mTabItem.setText(name);
+ }
+ });
+
+ // we're done with this ps.
+ mDone = true;
+ return;
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Interface implemented by the LogCatView in Eclipse for particular action on double-click.
+ */
+ public interface LogCatViewInterface {
+ public void onDoubleClick();
+ }
+
+ /**
+ * Create the log view with some default parameters
+ * @param colors The display color object
+ * @param filterStorage the storage for user defined filters.
+ * @param mode The filtering mode
+ */
+ public LogPanel(LogColors colors,
+ ILogFilterStorageManager filterStorage, int mode) {
+ mColors = colors;
+ mFilterMode = mode;
+ mFilterStorage = filterStorage;
+ mStore = DdmUiPreferences.getStore();
+ }
+
+ public void setActions(ICommonAction deleteAction, ICommonAction editAction,
+ ICommonAction[] logLevelActions) {
+ mDeleteFilterAction = deleteAction;
+ mEditFilterAction = editAction;
+ mLogLevelActions = logLevelActions;
+ }
+
+ /**
+ * Sets the column mode. Must be called before creatUI
+ * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and
+ * COLUMN_MODE_AUTO
+ */
+ public void setColumnMode(int mode) {
+ mColumnMode = mode;
+ }
+
+ /**
+ * Sets the display font.
+ * @param font The display font.
+ */
+ public void setFont(Font font) {
+ mDisplayFont = font;
+
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ Table table = f.getTable();
+ if (table != null) {
+ table.setFont(font);
+ }
+ }
+ }
+
+ if (mDefaultFilter != null) {
+ Table table = mDefaultFilter.getTable();
+ if (table != null) {
+ table.setFont(font);
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ startLogCat(getCurrentDevice());
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+ top.setLayout(new GridLayout(1, false));
+
+ // create the tab folder
+ mFolders = new TabFolder(top, SWT.NONE);
+ mFolders.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mFolders.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mCurrentFilter != null) {
+ mCurrentFilter.setSelectedState(false);
+ }
+ mCurrentFilter = getCurrentFilter();
+ mCurrentFilter.setSelectedState(true);
+ updateColumns(mCurrentFilter.getTable());
+ if (mCurrentFilter.getTempFilterStatus()) {
+ initFilter(mCurrentFilter);
+ }
+ selectionChanged(mCurrentFilter);
+ }
+ });
+
+
+ Composite bottom = new Composite(top, SWT.NONE);
+ bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ bottom.setLayout(new GridLayout(3, false));
+
+ Label label = new Label(bottom, SWT.NONE);
+ label.setText("Filter:");
+
+ final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER);
+ filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ filterText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ updateFilteringWith(filterText.getText());
+ }
+ });
+
+ /*
+ Button addFilterBtn = new Button(bottom, SWT.NONE);
+ addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$
+ addFilterBtn.getDisplay()));
+ */
+
+ // get the filters
+ createFilters();
+
+ // for each filter, create a tab.
+ int index = 0;
+
+ if (mDefaultFilter != null) {
+ createTab(mDefaultFilter, index++, false);
+ }
+
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ createTab(f, index++, false);
+ }
+ }
+
+ return top;
+ }
+
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ /**
+ * Sets the focus to the proper object.
+ */
+ @Override
+ public void setFocus() {
+ mFolders.setFocus();
+ }
+
+
+ /**
+ * Starts a new logcat and set mCurrentLogCat as the current receiver.
+ * @param device the device to connect logcat to.
+ */
+ public void startLogCat(final IDevice device) {
+ if (device == mCurrentLoggedDevice) {
+ return;
+ }
+
+ // if we have a logcat already running
+ if (mCurrentLoggedDevice != null) {
+ stopLogCat(false);
+ mCurrentLoggedDevice = null;
+ }
+
+ resetUI(false);
+
+ if (device != null) {
+ // create a new output receiver
+ mCurrentLogCat = new LogCatOuputReceiver();
+
+ // start the logcat in a different thread
+ new Thread("Logcat") { //$NON-NLS-1$
+ @Override
+ public void run() {
+
+ while (device.isOnline() == false &&
+ mCurrentLogCat != null &&
+ mCurrentLogCat.isCancelled == false) {
+ try {
+ sleep(2000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) {
+ // logcat was stopped/cancelled before the device became ready.
+ return;
+ }
+
+ try {
+ mCurrentLoggedDevice = device;
+ device.executeShellCommand("logcat -v long", mCurrentLogCat, 0 /*timeout*/); //$NON-NLS-1$
+ } catch (Exception e) {
+ Log.e("Logcat", e);
+ } finally {
+ // at this point the command is terminated.
+ mCurrentLogCat = null;
+ mCurrentLoggedDevice = null;
+ }
+ }
+ }.start();
+ }
+ }
+
+ /** Stop the current logcat */
+ public void stopLogCat(boolean inUiThread) {
+ if (mCurrentLogCat != null) {
+ mCurrentLogCat.isCancelled = true;
+
+ // when the thread finishes, no one will reference that object
+ // and it'll be destroyed
+ mCurrentLogCat = null;
+
+ // reset the content buffer
+ for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+ mBuffer[i] = null;
+ }
+
+ // because it's a circular buffer, it's hard to know if
+ // the array is empty with both start/end at 0 or if it's full
+ // with both start/end at 0 as well. So to mean empty, we use -1
+ mBufferStart = -1;
+ mBufferEnd = -1;
+
+ resetFilters();
+ resetUI(inUiThread);
+ }
+ }
+
+ /**
+ * Adds a new Filter. This methods displays the UI to create the filter
+ * and set up its parameters.<br>
+ * <b>MUST</b> be called from the ui thread.
+ *
+ */
+ public void addFilter() {
+ EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell());
+ if (dlg.open()) {
+ synchronized (mBuffer) {
+ // get the new filter in the array
+ LogFilter filter = dlg.getFilter();
+ addFilterToArray(filter);
+
+ int index = mFilters.length - 1;
+ if (mDefaultFilter != null) {
+ index++;
+ }
+
+ if (false) {
+
+ for (LogFilter f : mFilters) {
+ if (f.uiReady()) {
+ f.dispose();
+ }
+ }
+ if (mDefaultFilter != null && mDefaultFilter.uiReady()) {
+ mDefaultFilter.dispose();
+ }
+
+ // for each filter, create a tab.
+ int i = 0;
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ createTab(f, i++, true);
+ }
+ }
+ if (mDefaultFilter != null) {
+ createTab(mDefaultFilter, i++, true);
+ }
+ } else {
+
+ // create ui for the filter.
+ createTab(filter, index, true);
+
+ // reset the default as it shouldn't contain the content of
+ // this new filter.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+ }
+
+ // select the new filter
+ if (mCurrentFilter != null) {
+ mCurrentFilter.setSelectedState(false);
+ }
+ mFolders.setSelection(index);
+ filter.setSelectedState(true);
+ mCurrentFilter = filter;
+
+ selectionChanged(filter);
+
+ // finally we update the filtering mode if needed
+ if (mFilterMode == FILTER_NONE) {
+ mFilterMode = FILTER_MANUAL;
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+
+ }
+ }
+ }
+
+ /**
+ * Edits the current filter. The method displays the UI to edit the filter.
+ */
+ public void editFilter() {
+ if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+ EditFilterDialog dlg = new EditFilterDialog(
+ mFolders.getShell(), mCurrentFilter);
+ if (dlg.open()) {
+ synchronized (mBuffer) {
+ // at this point the filter has been updated.
+ // so we update its content
+ initFilter(mCurrentFilter);
+
+ // and the content of the "other" filter as well.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+ }
+ }
+ }
+ }
+
+ /**
+ * Deletes the current filter.
+ */
+ public void deleteFilter() {
+ synchronized (mBuffer) {
+ if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+ // remove the filter from the list
+ removeFilterFromArray(mCurrentFilter);
+ mCurrentFilter.dispose();
+
+ // select the new filter
+ mFolders.setSelection(0);
+ if (mFilters.length > 0) {
+ mCurrentFilter = mFilters[0];
+ } else {
+ mCurrentFilter = mDefaultFilter;
+ }
+
+ selectionChanged(mCurrentFilter);
+
+ // update the content of the "other" filter to include what was filtered out
+ // by the deleted filter.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+ }
+ }
+ }
+
+ /**
+ * saves the current selection in a text file.
+ * @return false if the saving failed.
+ */
+ public boolean save() {
+ synchronized (mBuffer) {
+ FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE);
+ String fileName;
+
+ dlg.setText("Save log...");
+ dlg.setFileName("log.txt");
+ String defaultPath = mDefaultLogSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+ dlg.setFilterPath(defaultPath);
+ dlg.setFilterNames(new String[] {
+ "Text Files (*.txt)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.txt"
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ mDefaultLogSave = dlg.getFilterPath();
+
+ // get the current table and its selection
+ Table currentTable = mCurrentFilter.getTable();
+
+ int[] selection = currentTable.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // loop on the selection and output the file.
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(fileName);
+
+ for (int i : selection) {
+ TableItem item = currentTable.getItem(i);
+ LogMessage msg = (LogMessage)item.getData();
+ String line = msg.toString();
+ writer.write(line);
+ writer.write('\n');
+ }
+ writer.flush();
+
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Empty the current circular buffer.
+ */
+ public void clear() {
+ synchronized (mBuffer) {
+ for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+ mBuffer[i] = null;
+ }
+
+ mBufferStart = -1;
+ mBufferEnd = -1;
+
+ // now we clear the existing filters
+ for (LogFilter filter : mFilters) {
+ filter.clear();
+ }
+
+ // and the default one
+ if (mDefaultFilter != null) {
+ mDefaultFilter.clear();
+ }
+ }
+ }
+
+ /**
+ * Copies the current selection of the current filter as multiline text.
+ *
+ * @param clipboard The clipboard to place the copied content.
+ */
+ public void copy(Clipboard clipboard) {
+ // get the current table and its selection
+ Table currentTable = mCurrentFilter.getTable();
+
+ copyTable(clipboard, currentTable);
+ }
+
+ /**
+ * Selects all lines.
+ */
+ public void selectAll() {
+ Table currentTable = mCurrentFilter.getTable();
+ currentTable.selectAll();
+ }
+
+ /**
+ * Sets a TableFocusListener which will be notified when one of the tables
+ * gets or loses focus.
+ *
+ * @param listener
+ */
+ public void setTableFocusListener(ITableFocusListener listener) {
+ // record the global listener, to make sure table created after
+ // this call will still be setup.
+ mGlobalListener = listener;
+
+ // now we setup the existing filters
+ for (LogFilter filter : mFilters) {
+ Table table = filter.getTable();
+
+ addTableToFocusListener(table);
+ }
+
+ // and the default one
+ if (mDefaultFilter != null) {
+ addTableToFocusListener(mDefaultFilter.getTable());
+ }
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.
+ *
+ * @param table the Table object.
+ */
+ private void addTableToFocusListener(final Table table) {
+ // create the activator for this table
+ final IFocusedTableActivator activator = new IFocusedTableActivator() {
+ @Override
+ public void copy(Clipboard clipboard) {
+ copyTable(clipboard, table);
+ }
+
+ @Override
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mGlobalListener.focusLost(activator);
+ }
+ });
+ }
+
+ /**
+ * Copies the current selection of a Table into the provided Clipboard, as
+ * multi-line text.
+ *
+ * @param clipboard The clipboard to place the copied content.
+ * @param table The table to copy from.
+ */
+ private static void copyTable(Clipboard clipboard, Table table) {
+ int[] selection = table.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // all lines must be concatenated.
+ StringBuilder sb = new StringBuilder();
+
+ // loop on the selection and output the file.
+ for (int i : selection) {
+ TableItem item = table.getItem(i);
+ LogMessage msg = (LogMessage)item.getData();
+ String line = msg.toString();
+ sb.append(line);
+ sb.append('\n');
+ }
+
+ // now add that to the clipboard
+ clipboard.setContents(new Object[] {
+ sb.toString()
+ }, new Transfer[] {
+ TextTransfer.getInstance()
+ });
+ }
+
+ /**
+ * Sets the log level for the current filter, but does not save it.
+ * @param i
+ */
+ public void setCurrentFilterLogLevel(int i) {
+ LogFilter filter = getCurrentFilter();
+
+ filter.setLogLevel(i);
+
+ initFilter(filter);
+ }
+
+ /**
+ * Creates a new tab in the folderTab item. Must be called from the ui
+ * thread.
+ * @param filter The filter associated with the tab.
+ * @param index the index of the tab. if -1, the tab will be added at the
+ * end.
+ * @param fillTable If true the table is filled with the current content of
+ * the buffer.
+ * @return The TabItem object that was created.
+ */
+ private TabItem createTab(LogFilter filter, int index, boolean fillTable) {
+ synchronized (mBuffer) {
+ TabItem item = null;
+ if (index != -1) {
+ item = new TabItem(mFolders, SWT.NONE, index);
+ } else {
+ item = new TabItem(mFolders, SWT.NONE);
+ }
+ item.setText(filter.getName());
+
+ // set the control (the parent is the TabFolder item, always)
+ Composite top = new Composite(mFolders, SWT.NONE);
+ item.setControl(top);
+
+ top.setLayout(new FillLayout());
+
+ // create the ui, first the table
+ final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+ t.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ if (mLogCatViewInterface != null) {
+ mLogCatViewInterface.onDoubleClick();
+ }
+ }
+ });
+
+ if (mDisplayFont != null) {
+ t.setFont(mDisplayFont);
+ }
+
+ // give the ui objects to the filters.
+ filter.setWidgets(item, t);
+
+ t.setHeaderVisible(true);
+ t.setLinesVisible(false);
+
+ if (mGlobalListener != null) {
+ addTableToFocusListener(t);
+ }
+
+ // create a controllistener that will handle the resizing of all the
+ // columns (except the last) and of the table itself.
+ ControlListener listener = null;
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ listener = new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = t.getClientArea();
+
+ // get the size of all but the last column
+ int total = t.getColumn(0).getWidth();
+ total += t.getColumn(1).getWidth();
+ total += t.getColumn(2).getWidth();
+ total += t.getColumn(3).getWidth();
+
+ if (r.width > total) {
+ t.getColumn(4).setWidth(r.width-total);
+ }
+ }
+ };
+
+ t.addControlListener(listener);
+ }
+
+ // then its column
+ TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT,
+ "00-00 00:00:00", //$NON-NLS-1$
+ PREFS_TIME, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "", SWT.CENTER,
+ "D", //$NON-NLS-1$
+ PREFS_LEVEL, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "pid", SWT.LEFT,
+ "9999", //$NON-NLS-1$
+ PREFS_PID, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "tag", SWT.LEFT,
+ "abcdefgh", //$NON-NLS-1$
+ PREFS_TAG, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "Message", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$
+ PREFS_MESSAGE, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ // instead of listening on resize for the last column, we make
+ // it non resizable.
+ col.setResizable(false);
+ }
+
+ if (fillTable) {
+ initFilter(filter);
+ }
+ return item;
+ }
+ }
+
+ protected void updateColumns(Table table) {
+ if (table != null) {
+ int index = 0;
+ TableColumn col;
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_TIME));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_LEVEL));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_PID));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_TAG));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_MESSAGE));
+ }
+ }
+
+ public void resetUI(boolean inUiThread) {
+ if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+ if (inUiThread) {
+ mFolders.dispose();
+ mParent.pack(true);
+ createControl(mParent);
+ } else {
+ Display d = mFolders.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ mFolders.dispose();
+ mParent.pack(true);
+ createControl(mParent);
+ }
+ });
+ }
+ } else {
+ // the ui is static we just empty it.
+ if (mFolders.isDisposed() == false) {
+ if (inUiThread) {
+ emptyTables();
+ } else {
+ Display d = mFolders.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (mFolders.isDisposed() == false) {
+ emptyTables();
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Process new Log lines coming from {@link LogCatOuputReceiver}.
+ * @param lines the new lines
+ */
+ protected void processLogLines(String[] lines) {
+ // WARNING: this will not work if the string contains more line than
+ // the buffer holds.
+
+ if (lines.length > STRING_BUFFER_LENGTH) {
+ Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH");
+ }
+
+ // parse the lines and create LogMessage that are stored in a temporary list
+ final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>();
+
+ synchronized (mBuffer) {
+ for (String line : lines) {
+ // ignore empty lines.
+ if (line.length() > 0) {
+ // check for header lines.
+ Matcher matcher = sLogPattern.matcher(line);
+ if (matcher.matches()) {
+ // this is a header line, parse the header and keep it around.
+ mLastMessageInfo = new LogMessageInfo();
+
+ mLastMessageInfo.time = matcher.group(1);
+ mLastMessageInfo.pidString = matcher.group(2);
+ mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString);
+ mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4));
+ mLastMessageInfo.tag = matcher.group(5).trim();
+ } else {
+ // This is not a header line.
+ // Create a new LogMessage and process it.
+ LogMessage mc = new LogMessage();
+
+ if (mLastMessageInfo == null) {
+ // The first line of output wasn't preceded
+ // by a header line; make something up so
+ // that users of mc.data don't NPE.
+ mLastMessageInfo = new LogMessageInfo();
+ mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$
+ mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$
+ mLastMessageInfo.pid = 0;
+ mLastMessageInfo.logLevel = LogLevel.INFO;
+ mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$
+ }
+
+ // If someone printed a log message with
+ // embedded '\n' characters, there will
+ // one header line followed by multiple text lines.
+ // Use the last header that we saw.
+ mc.data = mLastMessageInfo;
+
+ // tabs seem to display as only 1 tab so we replace the leading tabs
+ // by 4 spaces.
+ mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // process the new LogMessage.
+ processNewMessage(mc);
+
+ // store the new LogMessage
+ newMessages.add(mc);
+ }
+ }
+ }
+
+ // if we don't have a pending Runnable that will do the refresh, we ask the Display
+ // to run one in the UI thread.
+ if (mPendingAsyncRefresh == false) {
+ mPendingAsyncRefresh = true;
+
+ try {
+ Display display = mFolders.getDisplay();
+
+ // run in sync because this will update the buffer start/end indices
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ asyncRefresh();
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed, we're probably quitting. Let's stop.
+ stopLogCat(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Refreshes the UI with new messages.
+ */
+ private void asyncRefresh() {
+ if (mFolders.isDisposed() == false) {
+ synchronized (mBuffer) {
+ try {
+ // the circular buffer has been updated, let have the filter flush their
+ // display with the new messages.
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ f.flush();
+ }
+ }
+
+ if (mDefaultFilter != null) {
+ mDefaultFilter.flush();
+ }
+ } finally {
+ // the pending refresh is done.
+ mPendingAsyncRefresh = false;
+ }
+ }
+ } else {
+ stopLogCat(true);
+ }
+ }
+
+ /**
+ * Processes a new Message.
+ * <p/>This adds the new message to the buffer, and gives it to the existing filters.
+ * @param newMessage
+ */
+ private void processNewMessage(LogMessage newMessage) {
+ // if we are in auto filtering mode, make sure we have
+ // a filter for this
+ if (mFilterMode == FILTER_AUTO_PID ||
+ mFilterMode == FILTER_AUTO_TAG) {
+ checkFilter(newMessage.data);
+ }
+
+ // compute the index where the message goes.
+ // was the buffer empty?
+ int messageIndex = -1;
+ if (mBufferStart == -1) {
+ messageIndex = mBufferStart = 0;
+ mBufferEnd = 1;
+ } else {
+ messageIndex = mBufferEnd;
+
+ // increment the next usable slot index
+ mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH;
+
+ // check we aren't overwriting start
+ if (mBufferEnd == mBufferStart) {
+ mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
+ }
+ }
+
+ LogMessage oldMessage = null;
+
+ // record the message that was there before
+ if (mBuffer[messageIndex] != null) {
+ oldMessage = mBuffer[messageIndex];
+ }
+
+ // then add the new one
+ mBuffer[messageIndex] = newMessage;
+
+ // give the new message to every filters.
+ boolean filtered = false;
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ filtered |= f.addMessage(newMessage, oldMessage);
+ }
+ }
+ if (filtered == false && mDefaultFilter != null) {
+ mDefaultFilter.addMessage(newMessage, oldMessage);
+ }
+ }
+
+ private void createFilters() {
+ if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) {
+ // unarchive the filters.
+ mFilters = mFilterStorage.getFilterFromStore();
+
+ // set the colors
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ f.setColors(mColors);
+ }
+ }
+
+ if (mFilterStorage.requiresDefaultFilter()) {
+ mDefaultFilter = new LogFilter("Log");
+ mDefaultFilter.setColors(mColors);
+ mDefaultFilter.setSupportsDelete(false);
+ mDefaultFilter.setSupportsEdit(false);
+ }
+ } else if (mFilterMode == FILTER_NONE) {
+ // if the filtering mode is "none", we create a single filter that
+ // will receive all
+ mDefaultFilter = new LogFilter("Log");
+ mDefaultFilter.setColors(mColors);
+ mDefaultFilter.setSupportsDelete(false);
+ mDefaultFilter.setSupportsEdit(false);
+ }
+ }
+
+ /** Checks if there's an automatic filter for this md and if not
+ * adds the filter and the ui.
+ * This must be called from the UI!
+ * @param md
+ * @return true if the filter existed already
+ */
+ private boolean checkFilter(final LogMessageInfo md) {
+ if (true)
+ return true;
+ // look for a filter that matches the pid
+ if (mFilterMode == FILTER_AUTO_PID) {
+ for (LogFilter f : mFilters) {
+ if (f.getPidFilter() == md.pid) {
+ return true;
+ }
+ }
+ } else if (mFilterMode == FILTER_AUTO_TAG) {
+ for (LogFilter f : mFilters) {
+ if (f.getTagFilter().equals(md.tag)) {
+ return true;
+ }
+ }
+ }
+
+ // if we reach this point, no filter was found.
+ // create a filter with a temporary name of the pid
+ final LogFilter newFilter = new LogFilter(md.pidString);
+ String name = null;
+ if (mFilterMode == FILTER_AUTO_PID) {
+ newFilter.setPidMode(md.pid);
+
+ // ask the monitor thread if it knows the pid.
+ name = mCurrentLoggedDevice.getClientName(md.pid);
+ } else {
+ newFilter.setTagMode(md.tag);
+ name = md.tag;
+ }
+ addFilterToArray(newFilter);
+
+ final String fname = name;
+
+ // create the tabitem
+ final TabItem newTabItem = createTab(newFilter, -1, true);
+
+ // if the name is unknown
+ if (fname == null) {
+ // we need to find the process running under that pid.
+ // launch a thread do a ps on the device
+ new Thread("remote PS") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create the receiver
+ PsOutputReceiver psor = new PsOutputReceiver(md.pid,
+ newFilter, newTabItem);
+
+ // execute ps
+ try {
+ mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$
+ } catch (IOException e) {
+ // Ignore
+ } catch (TimeoutException e) {
+ // Ignore
+ } catch (AdbCommandRejectedException e) {
+ // Ignore
+ } catch (ShellCommandUnresponsiveException e) {
+ // Ignore
+ }
+ }
+ }.start();
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a new filter to the current filter array, and set its colors
+ * @param newFilter The filter to add
+ */
+ private void addFilterToArray(LogFilter newFilter) {
+ // set the colors
+ newFilter.setColors(mColors);
+
+ // add it to the array.
+ if (mFilters != null && mFilters.length > 0) {
+ LogFilter[] newFilters = new LogFilter[mFilters.length+1];
+ System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length);
+ newFilters[mFilters.length] = newFilter;
+ mFilters = newFilters;
+ } else {
+ mFilters = new LogFilter[1];
+ mFilters[0] = newFilter;
+ }
+ }
+
+ private void removeFilterFromArray(LogFilter oldFilter) {
+ // look for the index
+ int index = -1;
+ for (int i = 0 ; i < mFilters.length ; i++) {
+ if (mFilters[i] == oldFilter) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index != -1) {
+ LogFilter[] newFilters = new LogFilter[mFilters.length-1];
+ System.arraycopy(mFilters, 0, newFilters, 0, index);
+ System.arraycopy(mFilters, index + 1, newFilters, index,
+ newFilters.length-index);
+ mFilters = newFilters;
+ }
+ }
+
+ /**
+ * Initialize the filter with already existing buffer.
+ * @param filter
+ */
+ private void initFilter(LogFilter filter) {
+ // is it empty
+ if (filter.uiReady() == false) {
+ return;
+ }
+
+ if (filter == mDefaultFilter) {
+ initDefaultFilter();
+ return;
+ }
+
+ filter.clear();
+
+ if (mBufferStart != -1) {
+ int max = mBufferEnd;
+ if (mBufferEnd < mBufferStart) {
+ max += STRING_BUFFER_LENGTH;
+ }
+
+ for (int i = mBufferStart; i < max; i++) {
+ int realItemIndex = i % STRING_BUFFER_LENGTH;
+
+ filter.addMessage(mBuffer[realItemIndex], null /* old message */);
+ }
+ }
+
+ filter.flush();
+ filter.resetTempFilteringStatus();
+ }
+
+ /**
+ * Refill the default filter. Not to be called directly.
+ * @see initFilter()
+ */
+ private void initDefaultFilter() {
+ mDefaultFilter.clear();
+
+ if (mBufferStart != -1) {
+ int max = mBufferEnd;
+ if (mBufferEnd < mBufferStart) {
+ max += STRING_BUFFER_LENGTH;
+ }
+
+ for (int i = mBufferStart; i < max; i++) {
+ int realItemIndex = i % STRING_BUFFER_LENGTH;
+ LogMessage msg = mBuffer[realItemIndex];
+
+ // first we check that the other filters don't take this message
+ boolean filtered = false;
+ for (LogFilter f : mFilters) {
+ filtered |= f.accept(msg);
+ }
+
+ if (filtered == false) {
+ mDefaultFilter.addMessage(msg, null /* old message */);
+ }
+ }
+ }
+
+ mDefaultFilter.flush();
+ mDefaultFilter.resetTempFilteringStatus();
+ }
+
+ /**
+ * Reset the filters, to handle change in device in automatic filter mode
+ */
+ private void resetFilters() {
+ // if we are in automatic mode, then we need to rmove the current
+ // filter.
+ if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+ mFilters = null;
+
+ // recreate the filters.
+ createFilters();
+ }
+ }
+
+
+ private LogFilter getCurrentFilter() {
+ int index = mFolders.getSelectionIndex();
+
+ // if mFilters is null or index is invalid, we return the default
+ // filter. It doesn't matter if that one is null as well, since we
+ // would return null anyway.
+ if (index == 0 || mFilters == null) {
+ return mDefaultFilter;
+ }
+
+ return mFilters[index-1];
+ }
+
+
+ private void emptyTables() {
+ for (LogFilter f : mFilters) {
+ f.getTable().removeAll();
+ }
+
+ if (mDefaultFilter != null) {
+ mDefaultFilter.getTable().removeAll();
+ }
+ }
+
+ protected void updateFilteringWith(String text) {
+ synchronized (mBuffer) {
+ // reset the temp filtering for all the filters
+ for (LogFilter f : mFilters) {
+ f.resetTempFiltering();
+ }
+ if (mDefaultFilter != null) {
+ mDefaultFilter.resetTempFiltering();
+ }
+
+ // now we need to figure out the new temp filtering
+ // split each word
+ String[] segments = text.split(" "); //$NON-NLS-1$
+
+ ArrayList<String> keywords = new ArrayList<String>(segments.length);
+
+ // loop and look for temp id/tag
+ int tempPid = -1;
+ String tempTag = null;
+ for (int i = 0 ; i < segments.length; i++) {
+ String s = segments[i];
+ if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$
+ // get the pid
+ String[] seg = s.split(":"); //$NON-NLS-1$
+ if (seg.length == 2) {
+ if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$
+ tempPid = Integer.valueOf(seg[1]);
+ }
+ }
+ } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$
+ String seg[] = segments[i].split(":"); //$NON-NLS-1$
+ if (seg.length == 2) {
+ tempTag = seg[1];
+ }
+ } else {
+ keywords.add(s);
+ }
+ }
+
+ // set the temp filtering in the filters
+ if (tempPid != -1 || tempTag != null || keywords.size() > 0) {
+ String[] keywordsArray = keywords.toArray(
+ new String[keywords.size()]);
+
+ for (LogFilter f : mFilters) {
+ if (tempPid != -1) {
+ f.setTempPidFiltering(tempPid);
+ }
+ if (tempTag != null) {
+ f.setTempTagFiltering(tempTag);
+ }
+ f.setTempKeywordFiltering(keywordsArray);
+ }
+
+ if (mDefaultFilter != null) {
+ if (tempPid != -1) {
+ mDefaultFilter.setTempPidFiltering(tempPid);
+ }
+ if (tempTag != null) {
+ mDefaultFilter.setTempTagFiltering(tempTag);
+ }
+ mDefaultFilter.setTempKeywordFiltering(keywordsArray);
+
+ }
+ }
+
+ initFilter(mCurrentFilter);
+ }
+ }
+
+ /**
+ * Called when the current filter selection changes.
+ * @param selectedFilter
+ */
+ private void selectionChanged(LogFilter selectedFilter) {
+ if (mLogLevelActions != null) {
+ // get the log level
+ int level = selectedFilter.getLogLevel();
+ for (int i = 0 ; i < mLogLevelActions.length; i++) {
+ ICommonAction a = mLogLevelActions[i];
+ if (i == level - 2) {
+ a.setChecked(true);
+ } else {
+ a.setChecked(false);
+ }
+ }
+ }
+
+ if (mDeleteFilterAction != null) {
+ mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete());
+ }
+ if (mEditFilterAction != null) {
+ mEditFilterAction.setEnabled(selectedFilter.supportsEdit());
+ }
+ }
+
+ public String getSelectedErrorLineMessage() {
+ Table table = mCurrentFilter.getTable();
+ int[] selection = table.getSelectionIndices();
+
+ if (selection.length == 1) {
+ TableItem item = table.getItem(selection[0]);
+ LogMessage msg = (LogMessage)item.getData();
+ if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN)
+ return msg.msg;
+ }
+ return null;
+ }
+
+ public void setLogCatViewInterface(LogCatViewInterface i) {
+ mLogCatViewInterface = i;
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java
new file mode 100644
index 0000000..15b8b56
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java
@@ -0,0 +1,1125 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.net;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.TablePanel;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.DatasetRenderingOrder;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.data.DefaultKeyedValues2D;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimePeriod;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.AbstractIntervalXYDataset;
+import org.jfree.data.xy.TableXYDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.ui.RectangleAnchor;
+import org.jfree.ui.TextAnchor;
+
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Formatter;
+import java.util.Iterator;
+
+/**
+ * Displays live network statistics for currently selected {@link Client}.
+ */
+public class NetworkPanel extends TablePanel {
+
+ // TODO: enable view of packets and bytes/packet
+ // TODO: add sash to resize chart and table
+ // TODO: let user edit tags to be meaningful
+
+ /** Amount of historical data to display. */
+ private static final long HISTORY_MILLIS = 30 * 1000;
+
+ private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title";
+ private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes";
+ private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets";
+ private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes";
+ private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets";
+
+ /** Path to network statistics on remote device. */
+ private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats";
+
+ private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY;
+
+ /** Colors used for tag series data. */
+ private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] {
+ java.awt.Color.decode("0x2bc4c1"), // teal
+ java.awt.Color.decode("0xD50F25"), // red
+ java.awt.Color.decode("0x3369E8"), // blue
+ java.awt.Color.decode("0xEEB211"), // orange
+ java.awt.Color.decode("0x00bd2e"), // green
+ java.awt.Color.decode("0xae26ae"), // purple
+ };
+
+ private Display mDisplay;
+
+ private Composite mPanel;
+
+ /** Header panel with configuration options. */
+ private Composite mHeader;
+
+ private Label mSpeedLabel;
+ private Combo mSpeedCombo;
+
+ /** Current sleep between each sample, from {@link #mSpeedCombo}. */
+ private long mSpeedMillis;
+
+ private Button mRunningButton;
+ private Button mResetButton;
+
+ /** Chart of recent network activity. */
+ private JFreeChart mChart;
+ private ChartComposite mChartComposite;
+
+ private ValueAxis mDomainAxis;
+
+ /** Data for total traffic (tag 0x0). */
+ private TimeSeriesCollection mTotalCollection;
+ private TimeSeries mRxTotalSeries;
+ private TimeSeries mTxTotalSeries;
+
+ /** Data for detailed tagged traffic. */
+ private LiveTimeTableXYDataset mRxDetailDataset;
+ private LiveTimeTableXYDataset mTxDetailDataset;
+
+ private XYAreaRenderer mTotalRenderer;
+ private StackedXYAreaRenderer2 mRenderer;
+
+ /** Table showing summary of network activity. */
+ private Table mTable;
+ private TableViewer mTableViewer;
+
+ /** UID of currently selected {@link Client}. */
+ private int mActiveUid = -1;
+
+ /** List of traffic flows being actively tracked. */
+ private ArrayList<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>();
+
+ private SampleThread mSampleThread;
+
+ private class SampleThread extends Thread {
+ private volatile boolean mFinish;
+
+ public void finish() {
+ mFinish = true;
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ while (!mFinish && !mDisplay.isDisposed()) {
+ performSample();
+
+ try {
+ Thread.sleep(mSpeedMillis);
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+ }
+ }
+
+ /** Last snapshot taken by {@link #performSample()}. */
+ private NetworkSnapshot mLastSnapshot;
+
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ mPanel = new Composite(parent, SWT.NONE);
+
+ final FormLayout formLayout = new FormLayout();
+ mPanel.setLayout(formLayout);
+
+ createHeader();
+ createChart();
+ createTable();
+
+ return mPanel;
+ }
+
+ /**
+ * Create header panel with configuration options.
+ */
+ private void createHeader() {
+
+ mHeader = new Composite(mPanel, SWT.NONE);
+ final RowLayout layout = new RowLayout();
+ layout.center = true;
+ mHeader.setLayout(layout);
+
+ mSpeedLabel = new Label(mHeader, SWT.NONE);
+ mSpeedLabel.setText("Speed:");
+ mSpeedCombo = new Combo(mHeader, SWT.PUSH);
+ mSpeedCombo.add("Fast (100ms)");
+ mSpeedCombo.add("Medium (250ms)");
+ mSpeedCombo.add("Slow (500ms)");
+ mSpeedCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateSpeed();
+ }
+ });
+
+ mSpeedCombo.select(1);
+ updateSpeed();
+
+ mRunningButton = new Button(mHeader, SWT.PUSH);
+ mRunningButton.setText("Start");
+ mRunningButton.setEnabled(false);
+ mRunningButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ final boolean alreadyRunning = mSampleThread != null;
+ updateRunning(!alreadyRunning);
+ }
+ });
+
+ mResetButton = new Button(mHeader, SWT.PUSH);
+ mResetButton.setText("Reset");
+ mResetButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ clearTrackedItems();
+ }
+ });
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(0);
+ data.left = new FormAttachment(0);
+ data.right = new FormAttachment(100);
+ mHeader.setLayoutData(data);
+ }
+
+ /**
+ * Create chart of recent network activity.
+ */
+ private void createChart() {
+
+ mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false);
+
+ // create backing datasets and series
+ mRxTotalSeries = new TimeSeries("RX total");
+ mTxTotalSeries = new TimeSeries("TX total");
+
+ mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+ mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+
+ mTotalCollection = new TimeSeriesCollection();
+ mTotalCollection.addSeries(mRxTotalSeries);
+ mTotalCollection.addSeries(mTxTotalSeries);
+
+ mRxDetailDataset = new LiveTimeTableXYDataset();
+ mTxDetailDataset = new LiveTimeTableXYDataset();
+
+ mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA);
+ mRenderer = new StackedXYAreaRenderer2();
+
+ final XYPlot xyPlot = mChart.getXYPlot();
+
+ xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
+
+ xyPlot.setDataset(0, mTotalCollection);
+ xyPlot.setDataset(1, mRxDetailDataset);
+ xyPlot.setDataset(2, mTxDetailDataset);
+ xyPlot.setRenderer(0, mTotalRenderer);
+ xyPlot.setRenderer(1, mRenderer);
+ xyPlot.setRenderer(2, mRenderer);
+
+ // we control domain axis manually when taking samples
+ mDomainAxis = xyPlot.getDomainAxis();
+ mDomainAxis.setAutoRange(false);
+
+ final NumberAxis axis = new NumberAxis();
+ axis.setNumberFormatOverride(new BytesFormat(true));
+ axis.setAutoRangeMinimumSize(50);
+ xyPlot.setRangeAxis(axis);
+ xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
+
+ // draw thick line to separate RX versus TX traffic
+ xyPlot.addRangeMarker(
+ new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2)));
+
+ // label to indicate that positive axis is RX traffic
+ final ValueMarker rxMarker = new ValueMarker(0);
+ rxMarker.setStroke(new java.awt.BasicStroke(0));
+ rxMarker.setLabel("RX");
+ rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f));
+ rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+ rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
+ rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
+ xyPlot.addRangeMarker(rxMarker);
+
+ // label to indicate that negative axis is TX traffic
+ final ValueMarker txMarker = new ValueMarker(0);
+ txMarker.setStroke(new java.awt.BasicStroke(0));
+ txMarker.setLabel("TX");
+ txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f));
+ txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+ txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT);
+ txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
+ xyPlot.addRangeMarker(txMarker);
+
+ mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true,
+ false, true);
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(mHeader);
+ data.left = new FormAttachment(0);
+ data.bottom = new FormAttachment(70);
+ data.right = new FormAttachment(100);
+ mChartComposite.setLayoutData(data);
+ }
+
+ /**
+ * Create table showing summary of network activity.
+ */
+ private void createTable() {
+ mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
+
+ final FormData data = new FormData();
+ data.top = new FormAttachment(mChartComposite);
+ data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER);
+ data.bottom = new FormAttachment(100);
+ mTable.setLayoutData(data);
+
+ mTable.setHeaderVisible(true);
+ mTable.setLinesVisible(true);
+
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null);
+ TableHelper.createTableColumn(
+ mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store);
+ TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_RX_BYTES, store);
+ TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_RX_PACKETS, store);
+ TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_TX_BYTES, store);
+ TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12),
+ PREFS_NETWORK_COL_TX_PACKETS, store);
+
+ mTableViewer = new TableViewer(mTable);
+ mTableViewer.setContentProvider(new ContentProvider());
+ mTableViewer.setLabelProvider(new LabelProvider());
+ }
+
+ /**
+ * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection.
+ */
+ private void updateSpeed() {
+ switch (mSpeedCombo.getSelectionIndex()) {
+ case 0:
+ mSpeedMillis = 100;
+ break;
+ case 1:
+ mSpeedMillis = 250;
+ break;
+ case 2:
+ mSpeedMillis = 500;
+ break;
+ }
+ }
+
+ /**
+ * Update if {@link SampleThread} should be actively running. Will create
+ * new thread or finish existing thread to match requested state.
+ */
+ private void updateRunning(boolean shouldRun) {
+ final boolean alreadyRunning = mSampleThread != null;
+ if (alreadyRunning && !shouldRun) {
+ mSampleThread.finish();
+ mSampleThread = null;
+
+ mRunningButton.setText("Start");
+ mHeader.pack();
+ } else if (!alreadyRunning && shouldRun) {
+ mSampleThread = new SampleThread();
+ mSampleThread.start();
+
+ mRunningButton.setText("Stop");
+ mHeader.pack();
+ }
+ }
+
+ @Override
+ public void setFocus() {
+ mPanel.setFocus();
+ }
+
+ private static java.awt.Color nextSeriesColor(int index) {
+ return SERIES_COLORS[index % SERIES_COLORS.length];
+ }
+
+ /**
+ * Find a {@link TrackedItem} that matches the requested UID and tag, or
+ * create one if none exists.
+ */
+ public TrackedItem findOrCreateTrackedItem(int uid, int tag) {
+ // try searching for existing item
+ for (TrackedItem item : mTrackedItems) {
+ if (item.uid == uid && item.tag == tag) {
+ return item;
+ }
+ }
+
+ // nothing found; create new item
+ final TrackedItem item = new TrackedItem(uid, tag);
+ if (item.isTotal()) {
+ item.color = TOTAL_COLOR;
+ item.label = "Total";
+ } else {
+ final int size = mTrackedItems.size();
+ item.color = nextSeriesColor(size);
+ Formatter formatter = new Formatter();
+ item.label = "0x" + formatter.format("%08x", tag);
+ formatter.close();
+ }
+
+ // create color chip to display as legend in table
+ item.colorImage = new Image(mDisplay, 20, 20);
+ final GC gc = new GC(item.colorImage);
+ gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color
+ .getRed(), item.color.getGreen(), item.color.getBlue()));
+ gc.fillRectangle(item.colorImage.getBounds());
+ gc.dispose();
+
+ mTrackedItems.add(item);
+ return item;
+ }
+
+ /**
+ * Clear all {@link TrackedItem} and chart history.
+ */
+ public void clearTrackedItems() {
+ mRxTotalSeries.clear();
+ mTxTotalSeries.clear();
+
+ mRxDetailDataset.clear();
+ mTxDetailDataset.clear();
+
+ mTrackedItems.clear();
+ mTableViewer.setInput(mTrackedItems);
+ }
+
+ /**
+ * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}.
+ */
+ private void updateSeriesPaint() {
+ for (TrackedItem item : mTrackedItems) {
+ final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label);
+ if (seriesIndex >= 0) {
+ mRenderer.setSeriesPaint(seriesIndex, item.color);
+ mRenderer.setSeriesFillPaint(seriesIndex, item.color);
+ }
+ }
+
+ // series data is always the same color
+ final int count = mTotalCollection.getSeriesCount();
+ for (int i = 0; i < count; i++) {
+ mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR);
+ mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR);
+ }
+ }
+
+ /**
+ * Traffic flow being actively tracked, uniquely defined by UID and tag. Can
+ * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for
+ * charting, and into summary statistics for {@link Table} display.
+ */
+ private class TrackedItem {
+ public final int uid;
+ public final int tag;
+
+ public java.awt.Color color;
+ public Image colorImage;
+
+ public String label;
+ public long rxBytes;
+ public long rxPackets;
+ public long txBytes;
+ public long txPackets;
+
+ public TrackedItem(int uid, int tag) {
+ this.uid = uid;
+ this.tag = tag;
+ }
+
+ public boolean isTotal() {
+ return tag == 0x0;
+ }
+
+ /**
+ * Record the given {@link NetworkSnapshot} delta, updating
+ * {@link TimeSeries} and summary statistics.
+ *
+ * @param time Timestamp when delta was observed.
+ * @param deltaMillis Time duration covered by delta, in milliseconds.
+ */
+ public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) {
+ final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis;
+ final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis;
+
+ // record values under correct series
+ if (isTotal()) {
+ mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond);
+ mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond);
+ } else {
+ mRxDetailDataset.addValue(rxBytesPerSecond, time, label);
+ mTxDetailDataset.addValue(-txBytesPerSecond, time, label);
+ }
+
+ rxBytes += delta.rxBytes;
+ rxPackets += delta.rxPackets;
+ txBytes += delta.txBytes;
+ txPackets += delta.txPackets;
+ }
+ }
+
+ @Override
+ public void deviceSelected() {
+ // treat as client selection to update enabled states
+ clientSelected();
+ }
+
+ @Override
+ public void clientSelected() {
+ mActiveUid = -1;
+
+ final Client client = getCurrentClient();
+ if (client != null) {
+ final int pid = client.getClientData().getPid();
+ try {
+ // map PID to UID from device
+ final UidParser uidParser = new UidParser();
+ getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser);
+ mActiveUid = uidParser.uid;
+ } catch (TimeoutException e) {
+ e.printStackTrace();
+ } catch (AdbCommandRejectedException e) {
+ e.printStackTrace();
+ } catch (ShellCommandUnresponsiveException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ clearTrackedItems();
+ updateRunning(false);
+
+ final boolean validUid = mActiveUid != -1;
+ mRunningButton.setEnabled(validUid);
+ }
+
+ @Override
+ public void clientChanged(Client client, int changeMask) {
+ // ignored
+ }
+
+ /**
+ * Take a snapshot from {@link #getCurrentDevice()}, recording any delta
+ * network traffic to {@link TrackedItem}.
+ */
+ public void performSample() {
+ final IDevice device = getCurrentDevice();
+ if (device == null) return;
+
+ try {
+ final NetworkSnapshotParser parser = new NetworkSnapshotParser();
+ device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser);
+
+ if (parser.isError()) {
+ mDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ updateRunning(false);
+
+ final String title = "Problem reading stats";
+ final String message = "Problem reading xt_qtaguid network "
+ + "statistics from selected device.";
+ Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null);
+ ErrorDialog.openError(mPanel.getShell(), title, title, status);
+ }
+ });
+
+ return;
+ }
+
+ final NetworkSnapshot snapshot = parser.getParsedSnapshot();
+
+ // use first snapshot as baseline
+ if (mLastSnapshot == null) {
+ mLastSnapshot = snapshot;
+ return;
+ }
+
+ final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot);
+ mLastSnapshot = snapshot;
+
+ // perform delta updates over on UI thread
+ if (!mDisplay.isDisposed()) {
+ mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp));
+ }
+
+ } catch (TimeoutException e) {
+ e.printStackTrace();
+ } catch (AdbCommandRejectedException e) {
+ e.printStackTrace();
+ } catch (ShellCommandUnresponsiveException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Task that updates UI with given {@link NetworkSnapshot} delta.
+ */
+ private class UpdateDeltaRunnable implements Runnable {
+ private final NetworkSnapshot mDelta;
+ private final long mEndTime;
+
+ public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
+ mDelta = delta;
+ mEndTime = endTime;
+ }
+
+ @Override
+ public void run() {
+ if (mDisplay.isDisposed()) return;
+
+ final Millisecond time = new Millisecond(new Date(mEndTime));
+ for (NetworkSnapshot.Entry entry : mDelta) {
+ if (mActiveUid != entry.uid) continue;
+
+ final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag);
+ item.recordDelta(time, mDelta.timestamp, entry);
+ }
+
+ // remove any historical detail data
+ final long beforeMillis = mEndTime - HISTORY_MILLIS;
+ mRxDetailDataset.removeBefore(beforeMillis);
+ mTxDetailDataset.removeBefore(beforeMillis);
+
+ // trigger refresh from bulk changes above
+ mRxDetailDataset.fireDatasetChanged();
+ mTxDetailDataset.fireDatasetChanged();
+
+ // update axis to show latest 30 second time period
+ mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime);
+
+ updateSeriesPaint();
+
+ // kick table viewer to update
+ mTableViewer.setInput(mTrackedItems);
+ }
+ }
+
+ /**
+ * Parser that extracts UID from remote {@code /proc/pid/status} file.
+ */
+ private static class UidParser extends MultiLineReceiver {
+ public int uid = -1;
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.startsWith("Uid:")) {
+ // we care about the "real" UID
+ final String[] cols = line.split("\t");
+ uid = Integer.parseInt(cols[1]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parser that populates {@link NetworkSnapshot} based on contents of remote
+ * {@link NetworkPanel#PROC_XT_QTAGUID} file.
+ */
+ private static class NetworkSnapshotParser extends MultiLineReceiver {
+ private NetworkSnapshot mSnapshot;
+
+ public NetworkSnapshotParser() {
+ mSnapshot = new NetworkSnapshot(System.currentTimeMillis());
+ }
+
+ public boolean isError() {
+ return mSnapshot == null;
+ }
+
+ public NetworkSnapshot getParsedSnapshot() {
+ return mSnapshot;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.endsWith("No such file or directory")) {
+ mSnapshot = null;
+ return;
+ }
+
+ // ignore header line
+ if (line.startsWith("idx")) {
+ continue;
+ }
+
+ final String[] cols = line.split(" ");
+ if (cols.length < 9) continue;
+
+ // iface and set are currently ignored, which groups those
+ // entries together.
+ final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry();
+
+ entry.iface = null; //cols[1];
+ entry.uid = Integer.parseInt(cols[3]);
+ entry.set = -1; //Integer.parseInt(cols[4]);
+ entry.tag = kernelToTag(cols[2]);
+ entry.rxBytes = Long.parseLong(cols[5]);
+ entry.rxPackets = Long.parseLong(cols[6]);
+ entry.txBytes = Long.parseLong(cols[7]);
+ entry.txPackets = Long.parseLong(cols[8]);
+
+ mSnapshot.combine(entry);
+ }
+ }
+
+ /**
+ * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming
+ * format like {@code 0x7fffffff00000000}.
+ * Matches code in android.server.NetworkManagementSocketTagger
+ */
+ public static int kernelToTag(String string) {
+ int length = string.length();
+ if (length > 10) {
+ return Long.decode(string.substring(0, length - 8)).intValue();
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time.
+ */
+ private static class NetworkSnapshot implements Iterable<NetworkSnapshot.Entry> {
+ private ArrayList<Entry> mStats = new ArrayList<Entry>();
+
+ public final long timestamp;
+
+ /** Single parsed statistics row. */
+ public static class Entry {
+ public String iface;
+ public int uid;
+ public int set;
+ public int tag;
+ public long rxBytes;
+ public long rxPackets;
+ public long txBytes;
+ public long txPackets;
+
+ public boolean isEmpty() {
+ return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
+ }
+ }
+
+ public NetworkSnapshot(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public void clear() {
+ mStats.clear();
+ }
+
+ /**
+ * Combine the given {@link Entry} with any existing {@link Entry}, or
+ * insert if none exists.
+ */
+ public void combine(Entry entry) {
+ final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag);
+ if (existing != null) {
+ existing.rxBytes += entry.rxBytes;
+ existing.rxPackets += entry.rxPackets;
+ existing.txBytes += entry.txBytes;
+ existing.txPackets += entry.txPackets;
+ } else {
+ mStats.add(entry);
+ }
+ }
+
+ @Override
+ public Iterator<Entry> iterator() {
+ return mStats.iterator();
+ }
+
+ public Entry findEntry(String iface, int uid, int set, int tag) {
+ for (Entry entry : mStats) {
+ if (entry.uid == uid && entry.set == set && entry.tag == tag
+ && equal(entry.iface, iface)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Subtract the two given {@link NetworkSnapshot} objects, returning the
+ * delta between them.
+ */
+ public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) {
+ final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp);
+
+ // for each row on left, subtract value from right side
+ for (Entry leftEntry : left) {
+ final Entry rightEntry = right.findEntry(
+ leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag);
+ if (rightEntry == null) continue;
+
+ final Entry resultEntry = new Entry();
+ resultEntry.iface = leftEntry.iface;
+ resultEntry.uid = leftEntry.uid;
+ resultEntry.set = leftEntry.set;
+ resultEntry.tag = leftEntry.tag;
+ resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes;
+ resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets;
+ resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes;
+ resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets;
+
+ result.combine(resultEntry);
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Provider of {@link #mTrackedItems}.
+ */
+ private class ContentProvider implements IStructuredContentProvider {
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return mTrackedItems.toArray();
+ }
+ }
+
+ /**
+ * Provider of labels for {@Link TrackedItem} values.
+ */
+ private static class LabelProvider implements ITableLabelProvider {
+ private final DecimalFormat mFormat = new DecimalFormat("#,###");
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (element instanceof TrackedItem) {
+ final TrackedItem item = (TrackedItem) element;
+ switch (columnIndex) {
+ case 0:
+ return item.colorImage;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof TrackedItem) {
+ final TrackedItem item = (TrackedItem) element;
+ switch (columnIndex) {
+ case 0:
+ return null;
+ case 1:
+ return item.label;
+ case 2:
+ return mFormat.format(item.rxBytes);
+ case 3:
+ return mFormat.format(item.rxPackets);
+ case 4:
+ return mFormat.format(item.txBytes);
+ case 5:
+ return mFormat.format(item.txPackets);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Format that displays simplified byte units for when given values are
+ * large enough.
+ */
+ private static class BytesFormat extends NumberFormat {
+ private final String[] mUnits;
+ private final DecimalFormat mFormat = new DecimalFormat("#.#");
+
+ public BytesFormat(boolean perSecond) {
+ if (perSecond) {
+ mUnits = new String[] { "B/s", "KB/s", "MB/s" };
+ } else {
+ mUnits = new String[] { "B", "KB", "MB" };
+ }
+ }
+
+ @Override
+ public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+ double value = Math.abs(number);
+
+ int i = 0;
+ while (value > 1024 && i < mUnits.length - 1) {
+ value /= 1024;
+ i++;
+ }
+
+ toAppendTo.append(mFormat.format(value));
+ toAppendTo.append(mUnits[i]);
+
+ return toAppendTo;
+ }
+
+ @Override
+ public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+ return format((long) number, toAppendTo, pos);
+ }
+
+ @Override
+ public Number parse(String source, ParsePosition parsePosition) {
+ return null;
+ }
+ }
+
+ public static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /**
+ * Build stub string of requested length, usually for measurement.
+ */
+ private static String buildSampleText(int length) {
+ final StringBuilder builder = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ builder.append("X");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Dataset that contains live measurements. Exposes
+ * {@link #removeBefore(long)} to efficiently remove old data, and enables
+ * batched {@link #fireDatasetChanged()} events.
+ */
+ public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements
+ TableXYDataset {
+ private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true);
+
+ /**
+ * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+ */
+ public void addValue(Number value, TimePeriod rowKey, String columnKey) {
+ mValues.addValue(value, rowKey, columnKey);
+ }
+
+ /**
+ * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+ */
+ public void removeBefore(long beforeMillis) {
+ while(mValues.getRowCount() > 0) {
+ final TimePeriod period = (TimePeriod) mValues.getRowKey(0);
+ if (period.getEnd().getTime() < beforeMillis) {
+ mValues.removeRow(0);
+ } else {
+ break;
+ }
+ }
+ }
+
+ public int getColumnIndex(String key) {
+ return mValues.getColumnIndex(key);
+ }
+
+ public void clear() {
+ mValues.clear();
+ fireDatasetChanged();
+ }
+
+ @Override
+ public void fireDatasetChanged() {
+ super.fireDatasetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mValues.getRowCount();
+ }
+
+ @Override
+ public int getItemCount(int series) {
+ return mValues.getRowCount();
+ }
+
+ @Override
+ public int getSeriesCount() {
+ return mValues.getColumnCount();
+ }
+
+ @Override
+ public Comparable getSeriesKey(int series) {
+ return mValues.getColumnKey(series);
+ }
+
+ @Override
+ public double getXValue(int series, int item) {
+ final TimePeriod period = (TimePeriod) mValues.getRowKey(item);
+ return period.getStart().getTime();
+ }
+
+ @Override
+ public double getStartXValue(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public double getEndXValue(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getStartX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getEndX(int series, int item) {
+ return getXValue(series, item);
+ }
+
+ @Override
+ public Number getY(int series, int item) {
+ return mValues.getValue(item, series);
+ }
+
+ @Override
+ public Number getStartY(int series, int item) {
+ return getY(series, item);
+ }
+
+ @Override
+ public Number getEndY(int series, int item) {
+ return getY(series, item);
+ }
+ }
+}
diff --git a/ddms/ddmuilib/src/main/java/images/add.png b/ddms/ddmuilib/src/main/java/images/add.png
new file mode 100644
index 0000000..eefc2ca
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/add.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/android.png b/ddms/ddmuilib/src/main/java/images/android.png
new file mode 100644
index 0000000..3779d4d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/android.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/backward.png b/ddms/ddmuilib/src/main/java/images/backward.png
new file mode 100644
index 0000000..90a9713
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/backward.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/capture.png b/ddms/ddmuilib/src/main/java/images/capture.png
new file mode 100644
index 0000000..da5c10b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/capture.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/clear.png b/ddms/ddmuilib/src/main/java/images/clear.png
new file mode 100644
index 0000000..0009cf6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/clear.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/d.png b/ddms/ddmuilib/src/main/java/images/d.png
new file mode 100644
index 0000000..d45506e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/d.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-attach.png b/ddms/ddmuilib/src/main/java/images/debug-attach.png
new file mode 100644
index 0000000..9b8a11c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/debug-attach.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-error.png b/ddms/ddmuilib/src/main/java/images/debug-error.png
new file mode 100644
index 0000000..f22da1f
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/debug-error.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-wait.png b/ddms/ddmuilib/src/main/java/images/debug-wait.png
new file mode 100644
index 0000000..322be63
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/debug-wait.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/delete.png b/ddms/ddmuilib/src/main/java/images/delete.png
new file mode 100644
index 0000000..db5fab8
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/delete.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/device.png b/ddms/ddmuilib/src/main/java/images/device.png
new file mode 100644
index 0000000..7dbbbb6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/device.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/diff.png b/ddms/ddmuilib/src/main/java/images/diff.png
new file mode 100644
index 0000000..bdd9e5c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/diff.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/displayfilters.png b/ddms/ddmuilib/src/main/java/images/displayfilters.png
new file mode 100644
index 0000000..d110c2c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/displayfilters.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/down.png b/ddms/ddmuilib/src/main/java/images/down.png
new file mode 100644
index 0000000..f9426cb
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/down.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/e.png b/ddms/ddmuilib/src/main/java/images/e.png
new file mode 100644
index 0000000..dee7c97
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/e.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/edit.png b/ddms/ddmuilib/src/main/java/images/edit.png
new file mode 100644
index 0000000..b8f65bc
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/edit.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/empty.png b/ddms/ddmuilib/src/main/java/images/empty.png
new file mode 100644
index 0000000..f021542
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/empty.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/emulator.png b/ddms/ddmuilib/src/main/java/images/emulator.png
new file mode 100644
index 0000000..a718042
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/emulator.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/file.png b/ddms/ddmuilib/src/main/java/images/file.png
new file mode 100644
index 0000000..043a814
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/file.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/folder.png b/ddms/ddmuilib/src/main/java/images/folder.png
new file mode 100644
index 0000000..7e29b1a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/folder.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/forward.png b/ddms/ddmuilib/src/main/java/images/forward.png
new file mode 100644
index 0000000..a97a605
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/forward.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/gc.png b/ddms/ddmuilib/src/main/java/images/gc.png
new file mode 100644
index 0000000..5194806
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/gc.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/groupby.png b/ddms/ddmuilib/src/main/java/images/groupby.png
new file mode 100644
index 0000000..250b982
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/groupby.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/halt.png b/ddms/ddmuilib/src/main/java/images/halt.png
new file mode 100644
index 0000000..10e3720
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/halt.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/heap.png b/ddms/ddmuilib/src/main/java/images/heap.png
new file mode 100644
index 0000000..e3aa3f0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/heap.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/hprof.png b/ddms/ddmuilib/src/main/java/images/hprof.png
new file mode 100644
index 0000000..123d062
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/hprof.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/i.png b/ddms/ddmuilib/src/main/java/images/i.png
new file mode 100644
index 0000000..98385c5
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/i.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/importBug.png b/ddms/ddmuilib/src/main/java/images/importBug.png
new file mode 100644
index 0000000..f5da179
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/importBug.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/load.png b/ddms/ddmuilib/src/main/java/images/load.png
new file mode 100644
index 0000000..9e7bf6e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/load.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/pause.png b/ddms/ddmuilib/src/main/java/images/pause.png
new file mode 100644
index 0000000..19d286d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/pause.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/play.png b/ddms/ddmuilib/src/main/java/images/play.png
new file mode 100644
index 0000000..d54f013
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/play.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/pull.png b/ddms/ddmuilib/src/main/java/images/pull.png
new file mode 100644
index 0000000..f48f1b1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/pull.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/push.png b/ddms/ddmuilib/src/main/java/images/push.png
new file mode 100644
index 0000000..6222864
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/push.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/save.png b/ddms/ddmuilib/src/main/java/images/save.png
new file mode 100644
index 0000000..040ebda
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/save.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/scroll_lock.png b/ddms/ddmuilib/src/main/java/images/scroll_lock.png
new file mode 100644
index 0000000..5d26689
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/scroll_lock.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/sort_down.png b/ddms/ddmuilib/src/main/java/images/sort_down.png
new file mode 100644
index 0000000..2d4ccc1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/sort_down.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/sort_up.png b/ddms/ddmuilib/src/main/java/images/sort_up.png
new file mode 100644
index 0000000..3a0bc3c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/sort_up.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/thread.png b/ddms/ddmuilib/src/main/java/images/thread.png
new file mode 100644
index 0000000..ac839e8
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/thread.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/tracing_start.png b/ddms/ddmuilib/src/main/java/images/tracing_start.png
new file mode 100644
index 0000000..88771cc
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/tracing_start.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/tracing_stop.png b/ddms/ddmuilib/src/main/java/images/tracing_stop.png
new file mode 100644
index 0000000..71bd215
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/tracing_stop.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/up.png b/ddms/ddmuilib/src/main/java/images/up.png
new file mode 100644
index 0000000..92edf5a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/up.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/v.png b/ddms/ddmuilib/src/main/java/images/v.png
new file mode 100644
index 0000000..8044051
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/v.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/w.png b/ddms/ddmuilib/src/main/java/images/w.png
new file mode 100644
index 0000000..129d0f9
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/w.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/warning.png b/ddms/ddmuilib/src/main/java/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/warning.png
Binary files differ
diff --git a/ddms/ddmuilib/src/main/java/images/zygote.png b/ddms/ddmuilib/src/main/java/images/zygote.png
new file mode 100644
index 0000000..5cbb1d2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/images/zygote.png
Binary files differ
diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java
new file mode 100644
index 0000000..7894965
--- /dev/null
+++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmuilib.SysinfoPanel.BugReportParser;
+import com.android.ddmuilib.SysinfoPanel.BugReportParser.DataValue;
+import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.List;
+
+public class BugReportParserTest extends TestCase {
+ public void testParseEclairCpuDataSet() throws IOException {
+ String cpuInfo =
+ "Currently running services:\n" +
+ " cpuinfo\n" +
+ " ----------------------------------------------------------------------------\n" +
+ " DUMP OF SERVICE cpuinfo:\n" +
+ " Load: 0.53 / 0.11 / 0.04\n" +
+ " CPU usage from 33406ms to 28224ms ago:\n" +
+ " system_server: 56% = 42% user + 13% kernel / faults: 6724 minor 9 major\n" +
+ " bootanimation: 1% = 0% user + 0% kernel\n" +
+ " zygote: 0% = 0% user + 0% kernel / faults: 146 minor\n" +
+ " TOTAL: 98% = 67% user + 30% kernel;\n";
+ BufferedReader br = new BufferedReader(new StringReader(cpuInfo));
+ List<DataValue> data = BugReportParser.readCpuDataset(br);
+
+ assertEquals(4, data.size());
+ assertEquals("system_server (user)", data.get(0).name);
+ assertEquals("Idle", data.get(3).name);
+ }
+
+ public void testParseJbCpuDataSet() throws IOException {
+ String cpuInfo =
+ "Load: 1.0 / 1.02 / 0.97\n" +
+ "CPU usage from 96307ms to 36303ms ago:\n" +
+ " 0.4% 675/system_server: 0.3% user + 0.1% kernel / faults: 198 minor\n" +
+ " 0.1% 173/mpdecision: 0% user + 0.1% kernel\n" +
+ " 0% 2856/kworker/0:2: 0% user + 0% kernel\n" +
+ " 0% 3128/kworker/0:0: 0% user + 0% kernel\n" +
+ "0.3% TOTAL: 0.1% user + 0% kernel + 0% iowait\n";
+ BufferedReader br = new BufferedReader(new StringReader(cpuInfo));
+ List<DataValue> data = BugReportParser.readCpuDataset(br);
+
+ assertEquals(4, data.size());
+ assertEquals("675/system_server (user)", data.get(0).name);
+ assertEquals("Idle", data.get(3).name);
+ }
+
+ public void testParseProcRankEclair() throws IOException {
+ String memInfo =
+ " 51 39408K 37908K 18731K 14936K system_server\n" +
+ " 96 27432K 27432K 9501K 6816K android.process.acore\n" +
+ " 27 248K 248K 83K 76K /system/bin/debuggerd\n";
+ BufferedReader br = new BufferedReader(new StringReader(memInfo));
+ List<DataValue> data = BugReportParser.readProcRankDataset(br,
+ " PID Vss Rss Pss Uss cmdline\n");
+
+ assertEquals(3, data.size());
+ assertEquals("debuggerd", data.get(2).name);
+ if (data.get(0).value - 18731 > 0.0002) {
+ fail("Unexpected PSS Value " + data.get(0).value);
+ }
+ }
+
+ public void testParseProcRankJb() throws IOException {
+ String memInfo =
+ " 675 101120K 100928K 63452K 52624K system_server\n" +
+ "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" +
+ " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" +
+ " ------ ------ ------\n" +
+ " 480598K 394172K TOTAL\n" +
+ "\n" +
+ "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n";
+ BufferedReader br = new BufferedReader(new StringReader(memInfo));
+ List<DataValue> data = BugReportParser.readProcRankDataset(br,
+ " PID Vss Rss Pss Uss cmdline\n");
+
+ assertEquals(3, data.size());
+ }
+
+ public void testParseMeminfoEclair() throws IOException {
+ String memInfo =
+ "------ MEMORY INFO ------\n" +
+ "MemTotal: 516528 kB\n" +
+ "MemFree: 401036 kB\n" +
+ "Buffers: 0 kB\n" +
+ " PID Vss Rss Pss Uss cmdline\n" +
+ " 51 39408K 37908K 18731K 14936K system_server\n" +
+ " 96 27432K 27432K 9501K 6816K android.process.acore\n" +
+ " 297 23348K 23348K 5245K 2276K com.android.gallery\n";
+ BufferedReader br = new BufferedReader(new StringReader(memInfo));
+ List<DataValue> data = BugReportParser.readMeminfoDataset(br);
+ assertEquals(5, data.size());
+
+ assertEquals("Free", data.get(0).name);
+ }
+
+ public void testParseMeminfoJb() throws IOException {
+
+ String memInfo = // note: This dataset does not have all entries, so the totals will be off
+ "------ MEMORY INFO ------\n" +
+ "MemTotal: 1916984 kB\n" +
+ "MemFree: 888048 kB\n" +
+ "Buffers: 72036 kB\n" +
+ " PID Vss Rss Pss Uss cmdline\n" +
+ " 675 101120K 100928K 63452K 52624K system_server\n" +
+ "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" +
+ " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" +
+ " ------ ------ ------\n" +
+ " 480598K 394172K TOTAL\n" +
+ "\n" +
+ "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n";
+
+ BufferedReader br = new BufferedReader(new StringReader(memInfo));
+ List<DataValue> data = BugReportParser.readMeminfoDataset(br);
+
+ assertEquals(6, data.size());
+ }
+
+ public void testParseGfxInfo() throws IOException {
+ String gfxinfo =
+ "Applications Graphics Acceleration Info:\n" +
+ "Uptime: 78455570 Realtime: 78455565\n" +
+ "\n" +
+ "** Graphics info for pid 20517 [com.android.launcher] **\n" +
+ "\n" +
+ "Recent DisplayList operations\n" +
+ " DrawDisplayList\n" +
+ " <snip>\n" +
+ " RestoreToCount\n" +
+ "\n" +
+ "Caches:\n" +
+ "Current memory usage / total memory usage (bytes):\n" +
+ " TextureCache 4663920 / 25165824\n" +
+ " <snip>\n" +
+ " FontRenderer 0 262144 / 262144\n" +
+ "Other:\n" +
+ " FboCache 2 / 16\n" +
+ " PatchCache 9 / 512\n" +
+ "Total memory usage:\n" +
+ " 13274756 bytes, 12.66 MB\n" +
+ "\n" +
+ "Profile data in ms:\n" +
+ "\n" +
+ " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" +
+ " Draw Process Execute\n" +
+ " 0.85 1.10 0.61\n" +
+ " 54.45 0.85 0.52\n" +
+ " 1.04 2.17 0.73\n" +
+ " 0.15 0.46 1.01\n" +
+ "\n" +
+ "View hierarchy:\n" +
+ "\n" +
+ " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" +
+ " 276 views, 27.16 kB of display lists, 228 frames rendered\n" +
+ "\n" +
+ "\n" +
+ "Total ViewRootImpl: 1\n" +
+ "Total Views: 276\n" +
+ "Total DisplayList: 27.16 kB\n";
+
+ BufferedReader br = new BufferedReader(new StringReader(gfxinfo));
+ List<GfxProfileData> gfxProfile = BugReportParser.parseGfxInfo(br);
+
+ assertEquals(4, gfxProfile.size());
+ assertEquals(0.85, gfxProfile.get(0).draw);
+ assertEquals(1.01, gfxProfile.get(3).execute);
+ }
+}
diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java
new file mode 100644
index 0000000..4487454
--- /dev/null
+++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import junit.framework.TestCase;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import java.io.StringReader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+public class NativeHeapDataImporterTest extends TestCase {
+ private static final String BASIC_TEXT =
+ "Allocations: 1\n" +
+ "Size: 524292\n" +
+ "TotalSize: 524292\n" +
+ "BeginStacktrace:\n" +
+ " 40170bd8 /libc_malloc_leak.so --- getbacktrace --- /b/malloc_leak.c:258\n" +
+ " 400910d6 /lib/libc.so --- ca110c --- /bionic/malloc_debug_common.c:227\n" +
+ " 5dd6abfe /lib/libcgdrv.so --- 5dd6abfe ---\n" +
+ " 5dd98a8e /lib/libcgdrv.so --- 5dd98a8e ---\n" +
+ "EndStacktrace\n";
+
+ private NativeHeapDataImporter mImporter;
+
+ public void testImportValidAllocation() {
+ mImporter = createImporter(BASIC_TEXT);
+ try {
+ mImporter.run(new NullProgressMonitor());
+ } catch (InvocationTargetException e) {
+ fail("Unexpected exception while parsing text: " + e.getTargetException().getMessage());
+ } catch (InterruptedException e) {
+ fail("Tests are not interrupted!");
+ }
+
+ NativeHeapSnapshot snapshot = mImporter.getImportedSnapshot();
+ assertNotNull(snapshot);
+
+ // check whether all details have been parsed correctly
+ assertEquals(1, snapshot.getAllocations().size());
+
+ NativeAllocationInfo info = snapshot.getAllocations().get(0);
+
+ assertEquals(1, info.getAllocationCount());
+ assertEquals(524292, info.getSize());
+ assertEquals(true, info.isStackCallResolved());
+
+ List<NativeStackCallInfo> stack = info.getResolvedStackCall();
+ assertEquals(4, stack.size());
+ }
+
+ private NativeHeapDataImporter createImporter(String contentsToParse) {
+ StringReader r = new StringReader(contentsToParse);
+ return new NativeHeapDataImporter(r);
+ }
+}
diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java
new file mode 100644
index 0000000..e6c0e76
--- /dev/null
+++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class LogCatFilterSettingsSerializerTest extends TestCase {
+ /* test that decode(encode(f)) = f */
+ public void testSerializer() {
+ LogCatFilter fs = new LogCatFilter(
+ "TestFilter", //$NON-NLS-1$
+ "Tag'.*Regex", //$NON-NLS-1$
+ "regexForTextField..''", //$NON-NLS-1$
+ "123", //$NON-NLS-1$
+ "TestAppName.*", //$NON-NLS-1$
+ LogLevel.ERROR);
+
+ LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+ String s = serializer.encodeToPreferenceString(Arrays.asList(fs),
+ new HashMap<LogCatFilter, LogCatFilterData>());
+ List<LogCatFilter> decodedFiltersList = serializer.decodeFromPreferenceString(s);
+
+ assertEquals(1, decodedFiltersList.size());
+
+ LogCatFilter dfs = decodedFiltersList.get(0);
+ assertEquals(fs.getName(), dfs.getName());
+ assertEquals(fs.getTag(), dfs.getTag());
+ assertEquals(fs.getText(), dfs.getText());
+ assertEquals(fs.getPid(), dfs.getPid());
+ assertEquals(fs.getAppName(), dfs.getAppName());
+ assertEquals(fs.getLogLevel(), dfs.getLogLevel());
+ }
+
+ /* test that transient filters are not persisted */
+ public void testTransientFilters() {
+ LogCatFilter fs = new LogCatFilter(
+ "TestFilter", //$NON-NLS-1$
+ "Tag'.*Regex", //$NON-NLS-1$
+ "regexForTextField..''", //$NON-NLS-1$
+ "123", //$NON-NLS-1$
+ "TestAppName.*", //$NON-NLS-1$
+ LogLevel.ERROR);
+ LogCatFilterData fd = new LogCatFilterData(fs);
+ fd.setTransient();
+ HashMap<LogCatFilter, LogCatFilterData> fdMap =
+ new HashMap<LogCatFilter, LogCatFilterData>();
+ fdMap.put(fs, fd);
+
+ LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+ String s = serializer.encodeToPreferenceString(Arrays.asList(fs), fdMap);
+ List<LogCatFilter> decodedFiltersList = serializer.decodeFromPreferenceString(s);
+
+ assertEquals(0, decodedFiltersList.size());
+ }
+}
diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java
new file mode 100644
index 0000000..7d9869a
--- /dev/null
+++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import junit.framework.TestCase;
+
+public class LogCatStackTraceParserTest extends TestCase {
+ private LogCatStackTraceParser mTranslator;
+
+ private static final String SAMPLE_METHOD = "com.foo.Class.method"; //$NON-NLS-1$
+ private static final String SAMPLE_FNAME = "FileName"; //$NON-NLS-1$
+ private static final int SAMPLE_LINENUM = 20;
+ private static final String SAMPLE_TRACE =
+ String.format(" at %s(%s.groovy:%d)", //$NON-NLS-1$
+ SAMPLE_METHOD, SAMPLE_FNAME, SAMPLE_LINENUM);
+
+ @Override
+ protected void setUp() throws Exception {
+ mTranslator = new LogCatStackTraceParser();
+ }
+
+ public void testIsValidExceptionTrace() {
+ assertTrue(mTranslator.isValidExceptionTrace(SAMPLE_TRACE));
+ assertFalse(mTranslator.isValidExceptionTrace(
+ "java.lang.RuntimeException: message")); //$NON-NLS-1$
+ assertFalse(mTranslator.isValidExceptionTrace(
+ "at com.foo.test(Ins.java:unknown)")); //$NON-NLS-1$
+ }
+
+ public void testGetMethodName() {
+ assertEquals(SAMPLE_METHOD, mTranslator.getMethodName(SAMPLE_TRACE));
+ }
+
+ public void testGetFileName() {
+ assertEquals(SAMPLE_FNAME, mTranslator.getFileName(SAMPLE_TRACE));
+ }
+
+ public void testGetLineNumber() {
+ assertEquals(SAMPLE_LINENUM, mTranslator.getLineNumber(SAMPLE_TRACE));
+ }
+}
diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java
new file mode 100644
index 0000000..32a36c4
--- /dev/null
+++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmuilib.AbstractBufferFindTarget;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class RollingBufferFindTest extends TestCase {
+ public class FindTarget extends AbstractBufferFindTarget {
+ private int mSelectedItem = -1;
+ private int mItemReadCount = 0;
+ private List<String> mItems = Arrays.asList(
+ "abc",
+ "def",
+ "abc",
+ null,
+ "xyz"
+ );
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public String getItem(int index) {
+ mItemReadCount++;
+ return mItems.get(index);
+ }
+
+ @Override
+ public void selectAndReveal(int index) {
+ mSelectedItem = index;
+ }
+
+ @Override
+ public int getStartingIndex() {
+ return mItems.size() - 1;
+ }
+ }
+ FindTarget mFindTarget = new FindTarget();
+
+ public void testMultipleMatch() {
+ mFindTarget.mSelectedItem = -1;
+
+ String text = "abc";
+ int lastIndex = mFindTarget.mItems.lastIndexOf(text);
+ int firstIndex = mFindTarget.mItems.indexOf(text);
+
+ // the first time we search through the buffer we should hit the item at lastIndex
+ assertTrue(mFindTarget.findAndSelect(text, true, false));
+ assertEquals(lastIndex, mFindTarget.mSelectedItem);
+
+ // subsequent search should hit the item at first index
+ assertTrue(mFindTarget.findAndSelect(text, false, false));
+ assertEquals(firstIndex, mFindTarget.mSelectedItem);
+
+ // search again should roll over and hit the last index
+ assertTrue(mFindTarget.findAndSelect(text, false, false));
+ assertEquals(lastIndex, mFindTarget.mSelectedItem);
+ }
+
+ public void testMissingItem() {
+ mFindTarget.mSelectedItem = -1;
+ mFindTarget.mItemReadCount = 0;
+
+ // should not match
+ assertFalse(mFindTarget.findAndSelect("nonexistent", true, false));
+
+ // no item should be selected
+ assertEquals(-1, mFindTarget.mSelectedItem);
+
+ // but all items should have been read in once
+ assertEquals(mFindTarget.getItemCount(), mFindTarget.mItemReadCount);
+ }
+
+ public void testSearchDirection() {
+ String text = "abc";
+ int lastIndex = mFindTarget.mItems.lastIndexOf(text);
+ int firstIndex = mFindTarget.mItems.indexOf(text);
+
+ // the first time we search through the buffer we should hit the "abc" from the last
+ assertTrue(mFindTarget.findAndSelect(text, true, false));
+ assertEquals(lastIndex, mFindTarget.mSelectedItem);
+
+ // searching forward from there should also hit the first index
+ assertTrue(mFindTarget.findAndSelect(text, false, true));
+ assertEquals(firstIndex, mFindTarget.mSelectedItem);
+ }
+}