diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/.classpath b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.classpath
new file mode 100644
index 00000000..7e59275c
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/.project b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.project
new file mode 100644
index 00000000..2571ad4e
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.project
@@ -0,0 +1,34 @@
+
+
+ org.eclipse.andmore.ddmsuilib
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.core.resources.prefs b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000..99f26c02
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..c537b630
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.m2e.core.prefs b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000..f897a7f1
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.ddmsuilib/META-INF/MANIFEST.MF
new file mode 100644
index 00000000..4aba9679
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/META-INF/MANIFEST.MF
@@ -0,0 +1,27 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Ddmsuilib
+Bundle-SymbolicName: org.eclipse.andmore.ddmsuilib
+Bundle-Version: 1.0.0.qualifier
+Bundle-RequiredExecutionEnvironment: JavaSE-1.6
+Require-Bundle: org.eclipse.ui,
+ org.eclipse.core.runtime,
+ org.swtchart;bundle-version="0.7.0"
+Bundle-ClassPath: lib/ddmlib.jar,
+ .
+Export-Package: com.android.ddmlib,
+ com.android.ddmlib.log,
+ com.android.ddmlib.logcat,
+ com.android.ddmlib.testrunner,
+ com.android.ddmlib.utils,
+ com.android.ddmuilib,
+ com.android.ddmuilib.actions,
+ com.android.ddmuilib.annotation,
+ com.android.ddmuilib.console,
+ com.android.ddmuilib.explorer,
+ com.android.ddmuilib.handler,
+ com.android.ddmuilib.heap,
+ com.android.ddmuilib.location,
+ com.android.ddmuilib.log.event,
+ com.android.ddmuilib.logcat,
+ com.android.ddmuilib.net
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/build.properties b/android-core/plugins/org.eclipse.andmore.ddmsuilib/build.properties
new file mode 100644
index 00000000..f0ce74d6
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/build.properties
@@ -0,0 +1,5 @@
+source.. = src/main/java/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ lib/ddmlib.jar
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/add.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/add.png
new file mode 100644
index 00000000..eefc2ca3
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/add.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/android.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/android.png
new file mode 100644
index 00000000..3779d4d3
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/android.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/backward.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/backward.png
new file mode 100644
index 00000000..90a97137
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/backward.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/capture.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/capture.png
new file mode 100644
index 00000000..da5c10be
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/capture.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/clear.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/clear.png
new file mode 100644
index 00000000..0009cf66
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/clear.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/d.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/d.png
new file mode 100644
index 00000000..d45506ee
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/d.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-attach.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-attach.png
new file mode 100644
index 00000000..9b8a11c4
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-attach.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-error.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-error.png
new file mode 100644
index 00000000..f22da1ff
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-error.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-wait.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-wait.png
new file mode 100644
index 00000000..322be632
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/debug-wait.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/delete.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/delete.png
new file mode 100644
index 00000000..db5fab8e
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/delete.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/device.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/device.png
new file mode 100644
index 00000000..7dbbbb6a
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/device.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/diff.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/diff.png
new file mode 100644
index 00000000..bdd9e5c1
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/diff.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/displayfilters.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/displayfilters.png
new file mode 100644
index 00000000..d110c2cf
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/displayfilters.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/down.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/down.png
new file mode 100644
index 00000000..f9426cba
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/down.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/e.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/e.png
new file mode 100644
index 00000000..dee7c97f
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/e.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/edit.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/edit.png
new file mode 100644
index 00000000..b8f65bcd
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/edit.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/empty.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/empty.png
new file mode 100644
index 00000000..f0215424
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/empty.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/emulator.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/emulator.png
new file mode 100644
index 00000000..a7180428
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/emulator.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/file.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/file.png
new file mode 100644
index 00000000..043a8143
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/file.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/folder.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/folder.png
new file mode 100644
index 00000000..7e29b1a4
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/folder.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/forward.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/forward.png
new file mode 100644
index 00000000..a97a6056
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/forward.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/gc.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/gc.png
new file mode 100644
index 00000000..51948064
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/gc.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/groupby.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/groupby.png
new file mode 100644
index 00000000..250b9827
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/groupby.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/halt.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/halt.png
new file mode 100644
index 00000000..10e3720a
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/halt.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/heap.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/heap.png
new file mode 100644
index 00000000..e3aa3f06
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/heap.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/hprof.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/hprof.png
new file mode 100644
index 00000000..123d0620
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/hprof.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/i.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/i.png
new file mode 100644
index 00000000..98385c51
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/i.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/importBug.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/importBug.png
new file mode 100644
index 00000000..f5da179c
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/importBug.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/load.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/load.png
new file mode 100644
index 00000000..9e7bf6e7
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/load.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause.png
new file mode 100644
index 00000000..19d286d3
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause_logcat.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause_logcat.png
new file mode 100644
index 00000000..b5df287e
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pause_logcat.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/play.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/play.png
new file mode 100644
index 00000000..d54f013f
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/play.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pull.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pull.png
new file mode 100644
index 00000000..f48f1b1f
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/pull.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/push.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/push.png
new file mode 100644
index 00000000..6222864c
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/push.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/save.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/save.png
new file mode 100644
index 00000000..040ebda6
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/save.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_down.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_down.png
new file mode 100644
index 00000000..2d4ccc1a
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_down.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_up.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_up.png
new file mode 100644
index 00000000..3a0bc3cb
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/sort_up.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/thread.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/thread.png
new file mode 100644
index 00000000..ac839e89
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/thread.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_start.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_start.png
new file mode 100644
index 00000000..88771cc6
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_start.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_stop.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_stop.png
new file mode 100644
index 00000000..71bd215f
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/tracing_stop.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/up.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/up.png
new file mode 100644
index 00000000..92edf5a8
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/up.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/v.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/v.png
new file mode 100644
index 00000000..80440515
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/v.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/w.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/w.png
new file mode 100644
index 00000000..129d0f9c
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/w.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/warning.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/warning.png
new file mode 100644
index 00000000..ca3b6ede
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/warning.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/zygote.png b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/zygote.png
new file mode 100644
index 00000000..5cbb1d26
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/files/zygote.png differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/lib/ddmlib.jar b/android-core/plugins/org.eclipse.andmore.ddmsuilib/lib/ddmlib.jar
new file mode 100644
index 00000000..c9c2a9b2
Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddmsuilib/lib/ddmlib.jar differ
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/pom.xml b/android-core/plugins/org.eclipse.andmore.ddmsuilib/pom.xml
new file mode 100644
index 00000000..d955804e
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ org.eclipse.andmore.ddmsuilib
+ eclipse-plugin
+ ddmsuilib
+
+
+ ../../pom.xml
+ org.eclipse.andmore
+ andmore-core-parent
+ 0.5.0-SNAPSHOT
+
+
+
+
+ org.eclipse.tycho
+ tycho-source-plugin
+ ${tycho-version}
+
+
+ plugin-source
+
+ plugin-source
+
+
+
+
+
+
+
+
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/Addr2Line.java
new file mode 100644
index 00000000..10799ec0
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ * Each process can only handle one library, which should be provided when
+ * creating a new process.
+ *
+ * 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 getProcess(String library).
+ * 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 sProcessCache =
+ new HashMap();
+
+ /**
+ * 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 mLibrarySearchPaths = new ArrayList();
+
+ /**
+ * 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.
+ *
The library should be in a format that makes
+ * $ANDROID_PRODUCT_OUT + "/symbols" + library 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, null 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 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
new file mode 100644
index 00000000..23775e80
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
@@ -0,0 +1,662 @@
+/*
+ * 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_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$
+
+ private Composite mAllocationBase;
+ private Table mAllocationTable;
+ private TableViewer mAllocationViewer;
+
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+ private Button mEnableButton;
+ private Button mRequestButton;
+ 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_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ store);
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ topParent.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_ALLOC_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @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.
+ *
+ * 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.
+ *
+ * This must 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 null 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 null if none is found.
+ * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this
+ * selection is returned, otherwise, the ISelection returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or null
+ */
+ 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 results = new ArrayList();
+ // 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java
new file mode 100644
index 00000000..0ed4c950
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 isQuitting() 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java
new file mode 100644
index 00000000..3e66ea58
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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> mHeapMap;
+
+ /**
+ * Serialize the heap data into an array. The resulting array is available through
+ * getSerializedData().
+ * @param heapData The heap data to serialize
+ * @return true if the data changed.
+ */
+ protected boolean serializeHeapData(HeapData heapData) {
+ Collection 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> heapMap =
+ heapData.getProcessedHeapMap();
+ mHeapMap = heapMap;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the serialized heap data
+ */
+ protected byte[] getSerializedData() {
+ return mProcessedHeapData;
+ }
+
+ /**
+ * Processes and serialize the heapData.
+ *
+ * The resulting serialized array is {@link #mProcessedHeapData}.
+ *
+ * the resulting map is {@link #mHeapMap}.
+ * @param heapData the collection of {@link HeapSegment} that forms the heap data.
+ */
+ private void doSerializeHeapData(Collection heapData) {
+ mHeapMap = new TreeMap>();
+
+ Iterator 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 elementList = mHeapMap.get(v);
+ if (elementList == null) {
+ elementList = new ArrayList();
+ 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> elementLists = mHeapMap.values();
+ for (ArrayList 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java
new file mode 100644
index 00000000..a7119337
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java
new file mode 100644
index 00000000..db3642b1
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
new file mode 100644
index 00000000..68f23b7b
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
@@ -0,0 +1,830 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+/**
+ * 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 mListeners = new ArrayList();
+
+ private final ArrayList mDevicesToExpand = new ArrayList();
+
+ private boolean mAdvancedPortSupport;
+
+ /**
+ * A Content provider for the {@link TreeViewer}.
+ *
+ * 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 {
+ private static final String DEVICE_MODEL_PROPERTY = "ro.product.model"; //$NON-NLS-1$
+ private static final String DEVICE_MANUFACTURER_PROPERTY = "ro.product.manufacturer"; //$NON-NLS-1$
+
+ @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 getDeviceName(device);
+ 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) {
+ 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;
+ }
+
+ private String getDeviceName(IDevice device) {
+ StringBuilder sb = new StringBuilder(20);
+ sb.append(device.getSerialNumber());
+
+ if (device.isEmulator()) {
+ sb.append(String.format(" [%s]", device.getAvdName()));
+ } else {
+ String manufacturer = device.getProperty(DEVICE_MANUFACTURER_PROPERTY);
+ manufacturer = cleanupStringForDisplay(manufacturer);
+
+ String model = device.getProperty(DEVICE_MODEL_PROPERTY);
+ model = cleanupStringForDisplay(model);
+
+ boolean hasManufacturer = manufacturer.length() > 0;
+ boolean hasModel = model.length() > 0;
+ if (hasManufacturer || hasModel) {
+ sb.append(" ["); //$NON-NLS-1$
+ sb.append(manufacturer);
+
+ if (hasManufacturer && hasModel) {
+ sb.append(':');
+ }
+
+ sb.append(model);
+ sb.append(']');
+ }
+ }
+
+ return sb.toString();
+ }
+
+ private String cleanupStringForDisplay(String s) {
+ if (s == null) {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+
+ if (Character.isLetterOrDigit(c)) {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ @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.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java
new file mode 100644
index 00000000..82aed981
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
new file mode 100644
index 00000000..3f8e9892
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
@@ -0,0 +1,1307 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.swtchart.Chart;
+import org.swtchart.IBarSeries;
+import org.swtchart.ISeries.SeriesType;
+
+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 Chart mChart;
+ private Button mGcButton;
+
+ 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.
+ *
+ * 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(mStatisticsBase);
+
+ //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> heapMap =
+ client.getClientData().getVmHeapData().getProcessedHeapMap();
+
+ ArrayList list = heapMap.get(item.getData());
+ if (list != null) {
+ showChart(list);
+ }
+ }
+ }
+
+ }
+ });
+
+ return tab;
+ }
+
+ /**
+ * Creates the chart below the statistics table
+ */
+ private void createChart(Composite parent) {
+ mChart = new Chart(parent, SWT.NONE);
+
+// 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 font = mStatisticsBase.getFont();
+
+
+ mChart.getTitle().setText("Allocation count per size");
+ mChart.getTitle().setFont(font);
+
+ mChart.getAxisSet().getXAxis(0).getTitle().setText("Size");
+ mChart.getAxisSet().getYAxis(0).getTitle().setText("Count");
+
+ // create bar series
+// IBarSeries barSeries = (IBarSeries) chart.getSeriesSet()
+// .createSeries(SeriesType.BAR, "bar series");
+// barSeries.setYSeries();
+
+
+// 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
+
+ mChart.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // adjust the axis range
+ mChart.getAxisSet().adjustRange();
+
+ }
+
+ 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 iter = cd.getVmHeapIds();
+
+ while (iter.hasNext()) {
+ numRows++;
+ Integer id = iter.next();
+ Map 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> 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 selectedList = null;
+
+ // get the keys
+ Set 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 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 {
+ 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 list) {
+
+
+
+ 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<>> 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java
new file mode 100644
index 00000000..37dd9a03
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ImageLoader.java
new file mode 100644
index 00000000..fd480f64
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/filename
+ *
+ * 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 mLoadedImages = new HashMap();
+ private static final HashMap, ImageLoader> mInstances =
+ new HashMap, 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/filename, 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/filename.
+ *
+ * @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/filename.
+ *
+ * @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.
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/InfoPanel.java
new file mode 100644
index 00000000..60dc2c0e
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ *
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java
new file mode 100644
index 00000000..337bff29
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 mAllocations =
+ new ArrayList();
+
+ /** list of the {@link NativeAllocationInfo} being displayed based on the selection
+ * of {@link #mAllocDisplayCombo}.
+ */
+ private final ArrayList mDisplayedAllocations =
+ new ArrayList();
+
+ /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */
+ private final ArrayList mBackUpAllocations =
+ new ArrayList();
+
+ /** 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 mLibraryAllocations =
+ new ArrayList();
+
+ /* 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 mSourceCache =
+ new HashMap();
+ 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 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 list = info.getStackCallAddresses();
+ final int size = list.size();
+
+ ArrayList resolvedStackCall =
+ new ArrayList();
+
+ 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 mLibAllocations =
+ new ArrayList();
+
+ 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 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 addAllocation(), 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() {
+ @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
+ 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.
+ *
+ * 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 add = new ArrayList();
+
+ // 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 addresses = mi.getStackCallAddresses();
+ List 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 libcache =
+ new HashMap();
+
+ // 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() {
+ @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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/Panel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/Panel.java
new file mode 100644
index 00000000..d910cc74
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java
new file mode 100644
index 00000000..533372e8
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java
new file mode 100644
index 00000000..b0f885ad
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java
new file mode 100644
index 00000000..e6d2211c
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
new file mode 100644
index 00000000..336a5a34
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ * 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.
+ * UI creation is done through
+ * {@link #createPanel(Composite, String, String, String, String, String, IPreferenceStore)}.
+ *
+ */
+public final class StackTracePanel {
+
+ private static ISourceRevealer sSourceRevealer;
+
+ private Table mStackTraceTable;
+ private TableViewer mStackTraceViewer;
+
+ private Client mCurrentClient;
+
+
+ /**
+ * Content Provider to display the stack trace of a thread.
+ * Expected input is a {@link IStackTraceInfo} object.
+ */
+ private static class StackTraceContentProvider implements IStructuredContentProvider {
+ @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) {
+ StackTraceElement traceElement = (StackTraceElement)element;
+ switch (columnIndex) {
+ case 0:
+ return traceElement.getClassName();
+ case 1:
+ return traceElement.getMethodName();
+ case 2:
+ return traceElement.getFileName();
+ case 3:
+ return Integer.toString(traceElement.getLineNumber());
+ case 4:
+ return Boolean.toString(traceElement.isNativeMethod());
+ }
+ }
+
+ return null;
+ }
+
+ @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.
+ * This method will set the parent {@link Composite} to use a {@link GridLayout} with
+ * 2 columns.
+ * @param parent the parent composite.
+ * @param prefs_stack_col_class
+ * @param prefs_stack_col_method
+ * @param prefs_stack_col_file
+ * @param prefs_stack_col_line
+ * @param prefs_stack_col_native
+ * @param store
+ */
+ public Table createPanel(Composite parent, String prefs_stack_col_class,
+ String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line,
+ String prefs_stack_col_native, IPreferenceStore store) {
+
+ mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mStackTraceTable.setHeaderVisible(true);
+ mStackTraceTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Class",
+ SWT.LEFT,
+ "SomeLongClassName", //$NON-NLS-1$
+ prefs_stack_col_class, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Method",
+ SWT.LEFT,
+ "someLongMethod", //$NON-NLS-1$
+ prefs_stack_col_method, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "File",
+ SWT.LEFT,
+ "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$
+ prefs_stack_col_file, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Line",
+ SWT.RIGHT,
+ "99999", //$NON-NLS-1$
+ prefs_stack_col_line, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Native",
+ SWT.LEFT,
+ "Native", //$NON-NLS-1$
+ prefs_stack_col_native, store);
+
+ mStackTraceViewer = new TableViewer(mStackTraceTable);
+ mStackTraceViewer.setContentProvider(new StackTraceContentProvider());
+ mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider());
+
+ mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java
new file mode 100644
index 00000000..732de591
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java
new file mode 100644
index 00000000..4254f67a
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
new file mode 100644
index 00000000..23ad679c
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
@@ -0,0 +1,595 @@
+/*
+ * 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.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.andmore.charts.piechart.PieChart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+// PieChart implementation using SWTChart
+// https://eclipse.googlesource.com/linuxtools/org.eclipse.linuxtools/+/f6cf0d213bc791ca45b2e3b8f0c6c4d01375177f/profiling/org.eclipse.linuxtools.dataviewers.piechart/src/org/eclipse/linuxtools/dataviewers/piechart/PieChart.java
+
+
+/**
+ * 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 PieChart pieChart;
+
+ // The bugreport file to process
+ private File mDataFile;
+
+ // To get output from adb commands
+ private FileOutputStream mTempStream;
+
+ // Selects the current display: MODE_CPU, etc.
+ private int mMode = 0;
+
+ private static final int MODE_CPU = 0;
+ private static final int MODE_ALARM = 1;
+ private static final int MODE_WAKELOCK = 2;
+ private static final int MODE_MEMINFO = 3;
+ private static final int MODE_SYNC = 4;
+
+ // argument to dumpsys; section in the bugreport holding the data
+ private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm",
+ "batteryinfo", "MEMORY INFO", "content"};
+
+ private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
+ "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
+ "dumpsys content"};
+
+ private static final String CAPTIONS[] = {"CPU load", "Alarms",
+ "Wakelocks", "Memory usage", "Sync"};
+
+ /**
+ * Generates the dataset to display.
+ *
+ * @param file The bugreport file to process.
+ */
+ public void generateDataset(File file) {
+ //mDataset.clear();
+ mLabel.setText("");
+ if (file == null) {
+ return;
+ }
+ try {
+ BufferedReader br = getBugreportReader(file);
+ if (mMode == MODE_CPU) {
+ readCpuDataset(br);
+ } else if (mMode == MODE_ALARM) {
+ readAlarmDataset(br);
+ } else if (mMode == MODE_WAKELOCK) {
+ readWakelockDataset(br);
+ } else if (mMode == MODE_MEMINFO) {
+ readMeminfoDataset(br);
+ } else if (mMode == MODE_SYNC) {
+ readSyncDataset(br);
+ }
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed with
+ * {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ if (getCurrentDevice() != null) {
+ mFetchButton.setEnabled(true);
+ loadFromDevice();
+ } else {
+ mFetchButton.setEnabled(false);
+ }
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed with
+ * {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mDisplayMode.setFocus();
+ }
+
+ /**
+ * Fetches a new bugreport from the device and updates the display.
+ * Fetching is asynchronous. See also addOutput, flush, and isCancelled.
+ */
+ private void loadFromDevice() {
+ try {
+ initShellOutputBuffer();
+ if (mMode == MODE_MEMINFO) {
+ // Hack to add bugreport-style section header for meminfo
+ mTempStream.write("------ MEMORY INFO ------\n".getBytes());
+ }
+ getCurrentDevice().executeShellCommand(
+ DUMP_COMMAND[mMode], this);
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ } catch (TimeoutException e) {
+ Log.e("DDMS", e);
+ } catch (AdbCommandRejectedException e) {
+ Log.e("DDMS", e);
+ } catch (ShellCommandUnresponsiveException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Initializes temporary output file for executeShellCommand().
+ *
+ * @throws IOException on file error
+ */
+ void initShellOutputBuffer() throws IOException {
+ mDataFile = File.createTempFile("ddmsfile", ".txt");
+ mDataFile.deleteOnExit();
+ mTempStream = new FileOutputStream(mDataFile);
+ }
+
+ /**
+ * Adds output to the temp file. IShellOutputReceiver method. Called by
+ * executeShellCommand().
+ */
+ @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();
+ }
+ }
+ });
+
+ final Button loadButton = new Button(buttons, SWT.PUSH);
+ loadButton.setText("Load from File");
+ loadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(loadButton.getShell(),
+ SWT.OPEN);
+ fileDialog.setText("Load bugreport");
+ String filename = fileDialog.open();
+ if (filename != null) {
+ mDataFile = new File(filename);
+ generateDataset(mDataFile);
+ }
+ }
+ });
+
+ mFetchButton = new Button(buttons, SWT.PUSH);
+ mFetchButton.setText("Update from Device");
+ mFetchButton.setEnabled(false);
+ mFetchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ loadFromDevice();
+ }
+ });
+
+ mLabel = new Label(top, SWT.NONE);
+ mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+// mDataset = new DefaultPieDataset();
+ pieChart = new PieChart(top, SWT.BORDER);
+
+
+// ChartComposite chartComposite = new ChartComposite(top,
+// SWT.BORDER, chart,
+// ChartComposite.DEFAULT_HEIGHT,
+// ChartComposite.DEFAULT_HEIGHT,
+// ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+// ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+// 3000,
+// // max draw width. We don't want it to zoom, so we put a big number
+// 3000,
+// // max draw height. We don't want it to zoom, so we put a big number
+// true, // off-screen buffer
+// true, // properties
+// true, // save
+// true, // print
+// false, // zoom
+// true);
+ pieChart.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return top;
+ }
+
+ @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 {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ // Skip over the unwanted bugreport sections
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ Log.d("DDMS", "Service not found " + line);
+ break;
+ }
+ if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
+ line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
+ break;
+ }
+ }
+ return br;
+ }
+
+ /**
+ * Parse the time string generated by BatteryStats.
+ * A typical new-format string is "11d 13h 45m 39s 999ms".
+ * A typical old-format string is "12.3 sec".
+ * @return time in ms
+ */
+ private static long parseTimeMs(String s) {
+ long total = 0;
+ // Matches a single component e.g. "12.3 sec" or "45ms"
+ Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
+ Matcher m = p.matcher(s);
+ while (m.find()) {
+ String label = m.group(2);
+ if ("sec".equals(label)) {
+ // Backwards compatibility with old time format
+ total += (long) (Double.parseDouble(m.group(1)) * 1000);
+ continue;
+ }
+ long value = Integer.parseInt(m.group(1));
+ if ("d".equals(label)) {
+ total += value * 24 * 60 * 60 * 1000;
+ } else if ("h".equals(label)) {
+ total += value * 60 * 60 * 1000;
+ } else if ("m".equals(label)) {
+ total += value * 60 * 1000;
+ } else if ("s".equals(label)) {
+ total += value * 1000;
+ } else if ("ms".equals(label)) {
+ total += value;
+ }
+ }
+ return total;
+ }
+ /**
+ * Processes wakelock information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readWakelockDataset(BufferedReader br) throws IOException {
+ Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
+ Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
+ double total = 0;
+ boolean inCurrent = false;
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith("Current Battery Usage Statistics")) {
+ inCurrent = true;
+ } else if (inCurrent) {
+ Matcher m = lockPattern.matcher(line);
+ if (m.find()) {
+ double value = parseTimeMs(m.group(2)) / 1000.;
+// mDataset.setValue(m.group(1), value);
+ total -= value;
+ } else {
+ m = totalPattern.matcher(line);
+ if (m.find()) {
+ total += parseTimeMs(m.group(1)) / 1000.;
+ }
+ }
+ }
+ }
+ if (total > 0) {
+// mDataset.setValue("Unlocked", total);
+ }
+ }
+
+ /**
+ * Processes alarm information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readAlarmDataset(BufferedReader br) throws IOException {
+ Pattern pattern = Pattern
+ .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ long count = Long.parseLong(m.group(1));
+ String name = m.group(2);
+// mDataset.setValue(name, count);
+ }
+ }
+ }
+
+ /**
+ * Processes cpu load information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readCpuDataset(BufferedReader br) throws IOException {
+ Pattern pattern = Pattern
+ .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith("Load:")) {
+ mLabel.setText(line);
+ continue;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ String name = m.group(1);
+ long both = Long.parseLong(m.group(2));
+ long user = Long.parseLong(m.group(3));
+ long kernel = Long.parseLong(m.group(4));
+ if ("TOTAL".equals(name)) {
+ if (both < 100) {
+// mDataset.setValue("Idle", (100 - both));
+ }
+ } else {
+ // Try to make graphs more useful even with rounding;
+ // log often has 0% user + 0% kernel = 1% total
+ // We arbitrarily give extra to kernel
+ if (user > 0) {
+// mDataset.setValue(name + " (user)", user);
+ }
+ if (kernel > 0) {
+// mDataset.setValue(name + " (kernel)" , both - user);
+ }
+ if (user == 0 && kernel == 0 && both > 0) {
+// mDataset.setValue(name, both);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes meminfo information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readMeminfoDataset(BufferedReader br) throws IOException {
+ Pattern valuePattern = Pattern.compile("(\\d+) kB");
+ long total = 0;
+ long other = 0;
+ mLabel.setText("PSS in kB");
+
+ // Scan meminfo
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ // End of file
+ break;
+ }
+ Matcher m = valuePattern.matcher(line);
+ if (m.find()) {
+ long kb = Long.parseLong(m.group(1));
+ if (line.startsWith("MemTotal")) {
+ total = kb;
+ } else if (line.startsWith("MemFree")) {
+// mDataset.setValue("Free", kb);
+ total -= kb;
+ } else if (line.startsWith("Slab")) {
+// mDataset.setValue("Slab", kb);
+ total -= kb;
+ } else if (line.startsWith("PageTables")) {
+// mDataset.setValue("PageTables", kb);
+ total -= kb;
+ } else if (line.startsWith("Buffers") && kb > 0) {
+// mDataset.setValue("Buffers", kb);
+ total -= kb;
+ } else if (line.startsWith("Inactive")) {
+// mDataset.setValue("Inactive", kb);
+ total -= kb;
+ } else if (line.startsWith("MemFree")) {
+// mDataset.setValue("Free", kb);
+ total -= kb;
+ }
+ } else {
+ break;
+ }
+ }
+ // Scan procrank
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ break;
+ }
+ if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
+ // procrank header
+ continue;
+ }
+ if (line.indexOf("----") >= 0) {
+ //end of procrank section
+ break;
+ }
+ // Extract pss field from procrank output
+ long pss = Long.parseLong(line.substring(23, 31).trim());
+ String cmdline = line.substring(43).trim().replace("/system/bin/", "");
+ // Arbitrary minimum size to display
+ if (pss > 2000) {
+// mDataset.setValue(cmdline, pss);
+ } else {
+ other += pss;
+ }
+ total -= pss;
+ }
+// mDataset.setValue("Other", other);
+// mDataset.setValue("Unknown", total);
+ }
+
+ /**
+ * Processes sync information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readSyncDataset(BufferedReader br) throws IOException {
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith(" |") && line.length() > 70) {
+ String authority = line.substring(3, 18).trim();
+ String duration = line.substring(61, 70).trim();
+ // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
+ String durParts[] = duration.split(":");
+ if (durParts.length == 2) {
+ long dur = Long.parseLong(durParts[0]) * 60 + Long
+ .parseLong(durParts[1]);
+// mDataset.setValue(authority, dur);
+ } else if (duration.length() == 3) {
+ long dur = Long.parseLong(durParts[0]) * 3600
+ + Long.parseLong(durParts[1]) * 60 + Long
+ .parseLong(durParts[2]);
+// mDataset.setValue(authority, dur);
+ }
+ }
+ }
+ }
+}
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/TableHelper.java
new file mode 100644
index 00000000..66dcc0a4
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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
+ * PreferenceStore 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
+ * PreferenceStore 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
+ * PreferenceStore 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/TablePanel.java
new file mode 100644
index 00000000..c1eb7f6a
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ * 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.
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
new file mode 100644
index 00000000..f88b4c4a
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$
+
+ private Display mDisplay;
+ private Composite mBase;
+ private Label mNotEnabled;
+ private Label mNotSelected;
+
+ private Composite mThreadBase;
+ private Table mThreadTable;
+ private TableViewer mThreadViewer;
+
+ private Composite mStackTraceBase;
+ private Button mRefreshStackTraceButton;
+ private Label mStackTraceTimeLabel;
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+
+ /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */
+ private boolean mMustStopRecurringThreadUpdate = false;
+ /** Flag to tell the recurring thread update to stop running */
+ private boolean mRecurringThreadUpdateRunning = false;
+
+ private Object mLock = new Object();
+
+ private static final String[] THREAD_STATUS = {
+ "zombie", "running", "timed-wait", "monitor",
+ "wait", "init", "start", "native", "vmwait",
+ "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) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ updateThreadStackTrace(selectedThread);
+ }
+ });
+ mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ if (selectedThread != null) {
+ Client client = (Client)mThreadViewer.getInput();
+
+ if (client != null) {
+ client.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ // the separating sash
+ final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL);
+ Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+ sash.setBackground(darkGray);
+
+ // the UI below the sash
+ mStackTraceBase = new Composite(mThreadBase, SWT.NONE);
+ mStackTraceBase.setLayout(new GridLayout(2, false));
+
+ mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH);
+ mRefreshStackTraceButton.setText("Refresh");
+ mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ ThreadInfo selectedThread = getThreadSelection(null);
+ if (selectedThread != null) {
+ Client currentClient = getCurrentClient();
+ if (currentClient != null) {
+ currentClient.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
+ mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase,
+ PREFS_STACK_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ store);
+
+ GridData gd;
+ mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 2;
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mThreadTable.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_THREAD_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceBase.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ @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.
+ *
+ * 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();
+ }
+
+ /**
+ * Updates the stack call of the currently selected thread.
+ *
+ * This must 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 null 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 null if none is found.
+ * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection
+ * is returned, otherwise, the ISelection returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or null
+ */
+ 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java
new file mode 100644
index 00000000..856b874f
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 true to enable, and
+ * false 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java
new file mode 100644
index 00000000..c7fef324
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 true to enable, and
+ * false to disable
+ * @see ICommonAction#setChecked(boolean)
+ */
+ @Override
+ public void setChecked(boolean checked) {
+ item.setSelection(checked);
+ }
+
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled true to enable, and
+ * false 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java
new file mode 100644
index 00000000..8e9e11b6
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java
new file mode 100644
index 00000000..e767eda7
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java
new file mode 100644
index 00000000..4df4376e
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java
new file mode 100644
index 00000000..3679d413
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java
new file mode 100644
index 00000000..062d4f07
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java
new file mode 100644
index 00000000..b69d3b52
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 entries = new ArrayList();
+ 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java
new file mode 100644
index 00000000..1240e59b
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java
new file mode 100644
index 00000000..f50a94cf
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ * This must 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.
+ * This must 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.
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java
new file mode 100644
index 00000000..ab1b5f7f
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java
new file mode 100644
index 00000000..88db5cc7
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 allocations = new ArrayList();
+ 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 stackInfo = new ArrayList();
+ 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java
new file mode 100644
index 00000000..9eb6ddfb
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 commonAllocations =
+ new HashSet(oldSnapshot.getAllocations());
+ commonAllocations.retainAll(newSnapshot.getAllocations());
+
+ // Memory common between the old and new snapshots
+ mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations);
+ }
+
+ private static List getNewAllocations(NativeHeapSnapshot newSnapshot,
+ NativeHeapSnapshot oldSnapshot) {
+ Set allocations =
+ new HashSet(newSnapshot.getAllocations());
+ allocations.removeAll(oldSnapshot.getAllocations());
+ return new ArrayList(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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java
new file mode 100644
index 00000000..b96fa029
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
new file mode 100644
index 00000000..5f7abe29
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
@@ -0,0 +1,1152 @@
+/*
+ * 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.: /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 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 mDiffSnapshots;
+
+ private Map> 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();
+ mDiffSnapshots = new ArrayList();
+ mImportedSnapshotsPerPid = new HashMap>();
+ }
+
+ /** {@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 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 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 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 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 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 shallowCloneList(List allocations) {
+ List clonedList =
+ new ArrayList(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();
+ mDiffSnapshots = new ArrayList();
+
+ mSnapshotHeapButton.setEnabled(true);
+ mLoadHeapDataButton.setEnabled(true);
+
+ List importedSnapshots = mImportedSnapshotsPerPid.get(
+ c.getClientData().getPid());
+ if (importedSnapshots != null) {
+ for (NativeHeapSnapshot n : importedSnapshots) {
+ addNativeHeapSnapshot(n);
+ }
+ }
+
+ List 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 items = new ArrayList();
+
+ 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) {
+ parent.setLayout(new GridLayout(1, false));
+
+ 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 importedSnapshots = mImportedSnapshotsPerPid.get(pid);
+ if (importedSnapshots == null) {
+ importedSnapshots = new ArrayList();
+ }
+
+ 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 properties = Arrays.asList(new String[] {
+ "Library",
+ "Total",
+ "Percentage",
+ "Count",
+ "Size",
+ "Method",
+ });
+
+ List sampleValues = Arrays.asList(new String[] {
+ "/path/in/device/to/system/library.so",
+ "123456789",
+ " 100%",
+ "123456789",
+ "123456789",
+ "PossiblyLongDemangledMethodName",
+ });
+
+ // right align numeric values
+ List 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 properties = Arrays.asList(new String[] {
+ "Address",
+ "Library",
+ "Method",
+ "File",
+ "Line",
+ });
+
+ List 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 mCallSites;
+ private List mMappedLibraries;
+ private Map mResolvedSymbolCache;
+
+ public SymbolResolverTask(List callSites,
+ List mappedLibraries) {
+ mCallSites = callSites;
+ mMappedLibraries = mappedLibraries;
+
+ mResolvedSymbolCache = new HashMap();
+ }
+
+ @Override
+ public void run() {
+ for (NativeAllocationInfo callSite : mCallSites) {
+ if (callSite.isStackCallResolved()) {
+ continue;
+ }
+
+ List addresses = callSite.getStackCallAddresses();
+ List resolvedStackInfo =
+ new ArrayList(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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java
new file mode 100644
index 00000000..c31716b9
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 getAllocations() {
+ if (mDisplayZygoteMemory) {
+ return mNativeHeapDump.getAllocations();
+ } else {
+ return mNativeHeapDump.getNonZygoteAllocations();
+ }
+ }
+}
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java
new file mode 100644
index 00000000..b786bfaa
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 getLibraryAllocations(NativeHeapSnapshot snapshot) {
+ if (mDisplayZygoteMemory) {
+ return snapshot.getAllocationsByLibrary();
+ } else {
+ return snapshot.getNonZygoteAllocationsByLibrary();
+ }
+ }
+}
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java
new file mode 100644
index 00000000..e2023d2c
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 mHeapAllocations;
+ private List mHeapAllocationsByLibrary;
+
+ private List mNonZygoteHeapAllocations;
+ private List mNonZygoteHeapAllocationsByLibrary;
+
+ private long mTotalSize;
+
+ public NativeHeapSnapshot(List heapAllocations) {
+ mHeapAllocations = heapAllocations;
+
+ // precompute the total size as this is always needed.
+ mTotalSize = getTotalMemory(heapAllocations);
+ }
+
+ protected long getTotalMemory(Collection heapSnapshot) {
+ long total = 0;
+
+ for (NativeAllocationInfo info : heapSnapshot) {
+ total += info.getAllocationCount() * info.getSize();
+ }
+
+ return total;
+ }
+
+ public List getAllocations() {
+ return mHeapAllocations;
+ }
+
+ public List getAllocationsByLibrary() {
+ if (mHeapAllocationsByLibrary != null) {
+ return mHeapAllocationsByLibrary;
+ }
+
+ List 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 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 getNonZygoteAllocations() {
+ if (mNonZygoteHeapAllocations != null) {
+ return mNonZygoteHeapAllocations;
+ }
+
+ // filter out all zygote allocations
+ mNonZygoteHeapAllocations = new ArrayList();
+ for (NativeAllocationInfo info : mHeapAllocations) {
+ if (info.isZygoteChild()) {
+ mNonZygoteHeapAllocations.add(info);
+ }
+ }
+
+ return mNonZygoteHeapAllocations;
+ }
+
+ public List getNonZygoteAllocationsByLibrary() {
+ if (mNonZygoteHeapAllocationsByLibrary != null) {
+ return mNonZygoteHeapAllocationsByLibrary;
+ }
+
+ List heapAllocations =
+ NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations());
+
+ // cache for future uses only if it is fully resolved.
+ if (isFullyResolved(heapAllocations)) {
+ mNonZygoteHeapAllocationsByLibrary = heapAllocations;
+ }
+
+ return heapAllocations;
+ }
+}
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java
new file mode 100644
index 00000000..1722cdb7
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 mHeapAllocations;
+ private int mTotalSize;
+
+ private NativeLibraryAllocationInfo(String libraryName) {
+ mLibraryName = libraryName;
+ mHeapAllocations = new ArrayList();
+ }
+
+ 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 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 constructFrom(
+ List allocations) {
+ if (allocations == null) {
+ return null;
+ }
+
+ Map allocationsByLibrary =
+ new HashMap();
+
+ // 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 libraryAllocations =
+ new ArrayList(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() {
+ @Override
+ public int compare(NativeLibraryAllocationInfo o1,
+ NativeLibraryAllocationInfo o2) {
+ return (int) (o2.getTotalSize() - o1.getTotalSize());
+ }
+ });
+
+ return libraryAllocations;
+ }
+
+ private static void addtoLibrary(Map 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java
new file mode 100644
index 00000000..9a6ddb2a
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java
new file mode 100644
index 00000000..b7428b95
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java
new file mode 100644
index 00000000..1a75c6e4
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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 mCallSites;
+ private List mMappedLibraries;
+ private List mSymbolSearchFolders;
+
+ /** All unresolved addresses from all the callsites. */
+ private SortedSet mUnresolvedAddresses;
+
+ /** Set of all addresses that could were not resolved at the end of the resolution process. */
+ private Set mUnresolvableAddresses;
+
+ /** Map of library -> [unresolved addresses mapping to this library]. */
+ private Map> mUnresolvedAddressesPerLibrary;
+
+ /** Addresses that could not be mapped to a library, should be mostly empty. */
+ private Set mUnmappedAddresses;
+
+ /** Cache of the resolution for every unresolved address. */
+ private Map mAddressResolution;
+
+ /** List of libraries that were not located on disk. */
+ private Set mNotFoundLibraries;
+ private String mAddr2LineErrorMessage = null;
+
+ public NativeSymbolResolverTask(List callSites,
+ List mappedLibraries,
+ String symbolSearchPath) {
+ mCallSites = callSites;
+ mMappedLibraries = mappedLibraries;
+ mSymbolSearchFolders = new ArrayList();
+ mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER);
+ mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":")));
+
+ mUnresolvedAddresses = new TreeSet();
+ mUnresolvableAddresses = new HashSet();
+ mUnresolvedAddressesPerLibrary = new HashMap>();
+ mUnmappedAddresses = new HashSet();
+ mAddressResolution = new HashMap();
+ mNotFoundLibraries = new HashSet();
+ }
+
+ @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 mappedAddresses = new HashSet();
+
+ for (NativeLibraryMapInfo lib : mMappedLibraries) {
+ SortedSet 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 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 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 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 callSites) {
+ for (NativeAllocationInfo callSite : callSites) {
+ List stackInfo = new ArrayList();
+
+ 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 getUnmappedAddresses() {
+ return mUnmappedAddresses;
+ }
+
+ public Set getUnresolvableAddresses() {
+ return mUnresolvableAddresses;
+ }
+
+ public Set getNotFoundLibraries() {
+ return mNotFoundLibraries;
+ }
+}
diff --git a/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java
new file mode 100644
index 00000000..2aef53c2
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ * This handle the conversion between both modes automatically by using a {@link ModifyListener}
+ * on all the {@link Text} widgets.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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).
+ * 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/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/android-core/plugins/org.eclipse.andmore.ddmsuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java
new file mode 100644
index 00000000..a30337a7
--- /dev/null
+++ b/android-core/plugins/org.eclipse.andmore.ddmsuilib/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.
+ *
+ * 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 mWayPoints;
+ List