aboutsummaryrefslogtreecommitdiffstats
path: root/ddms/libs/ddmuilib/src
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
commit55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch)
treeecd18b995aea8eeeb8b3823266280d41245bf0f7 /ddms/libs/ddmuilib/src
parent82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff)
downloadsdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'ddms/libs/ddmuilib/src')
-rw-r--r--ddms/libs/ddmuilib/src/Android.mk22
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java278
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java487
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java50
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java193
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java33
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java79
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java744
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java1454
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java1294
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java45
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java38
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java86
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java62
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java175
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java1633
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java49
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java73
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java256
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java78
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java258
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java582
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java149
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java128
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java572
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java258
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java42
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java68
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java31
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java31
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java91
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java47
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java167
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java833
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java152
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java243
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java373
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java210
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java53
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java45
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java81
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java34
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java42
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java43
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java73
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java89
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java55
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java422
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java379
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java293
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java177
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java219
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java971
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java955
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java82
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java926
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java628
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java90
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java158
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java353
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java27
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java555
-rw-r--r--ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java1571
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/add.pngbin0 -> 146 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/android.pngbin0 -> 3609 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/backward.pngbin0 -> 136 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/clear.pngbin0 -> 217 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/d.pngbin0 -> 638 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-attach.pngbin0 -> 156 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-error.pngbin0 -> 222 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/debug-wait.pngbin0 -> 156 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/delete.pngbin0 -> 107 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/device.pngbin0 -> 135 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/down.pngbin0 -> 141 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/e.pngbin0 -> 511 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/edit.pngbin0 -> 223 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/empty.pngbin0 -> 75 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/emulator.pngbin0 -> 287 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/file.pngbin0 -> 157 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/folder.pngbin0 -> 123 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/forward.pngbin0 -> 137 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/gc.pngbin0 -> 165 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/halt.pngbin0 -> 197 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/heap.pngbin0 -> 222 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/i.pngbin0 -> 498 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/importBug.pngbin0 -> 191 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/load.pngbin0 -> 163 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/pause.pngbin0 -> 98 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/play.pngbin0 -> 138 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/pull.pngbin0 -> 329 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/push.pngbin0 -> 228 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/save.pngbin0 -> 240 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/thread.pngbin0 -> 121 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/up.pngbin0 -> 134 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/v.pngbin0 -> 587 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/w.pngbin0 -> 681 bytes
-rw-r--r--ddms/libs/ddmuilib/src/resources/images/warning.pngbin0 -> 147 bytes
97 files changed, 19685 insertions, 0 deletions
diff --git a/ddms/libs/ddmuilib/src/Android.mk b/ddms/libs/ddmuilib/src/Android.mk
new file mode 100644
index 0000000..acbda44
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/Android.mk
@@ -0,0 +1,22 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_RESOURCE_DIRS := resources
+
+LOCAL_JAVA_LIBRARIES := \
+ ddmlib \
+ swt \
+ org.eclipse.jface_3.2.0.I20060605-1400 \
+ org.eclipse.equinox.common_3.2.0.v20060603 \
+ org.eclipse.core.commands_3.2.0.I20060605-1400 \
+ jcommon-1.0.12 \
+ jfreechart-1.0.9 \
+ jfreechart-1.0.9-swt
+
+LOCAL_MODULE := ddmuilib
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java
new file mode 100644
index 0000000..a2f12d5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.*;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * Represents an addr2line process to get filename/method information from a
+ * memory address.<br>
+ * Each process can only handle one library, which should be provided when
+ * creating a new process.<br>
+ * <br>
+ * The processes take some time to load as they need to parse the library files.
+ * For this reason, processes cannot be manually started. Instead the class
+ * keeps an internal list of processes and one asks for a process for a specific
+ * library, using <code>getProcess(String library)<code>.<br></br>
+ * Internally, the processes are started in pipe mode to be able to query them
+ * with multiple addresses.
+ */
+public class Addr2Line {
+
+ /**
+ * Loaded processes list. This is also used as a locking object for any
+ * methods dealing with starting/stopping/creating processes/querying for
+ * method.
+ */
+ private static final HashMap<String, Addr2Line> sProcessCache =
+ new HashMap<String, Addr2Line>();
+
+ /**
+ * byte array representing a carriage return. Used to push addresses in the
+ * process pipes.
+ */
+ private static final byte[] sCrLf = {
+ '\n'
+ };
+
+ /** Path to the library */
+ private String mLibrary;
+
+ /** the command line process */
+ private Process mProcess;
+
+ /** buffer to read the result of the command line process from */
+ private BufferedReader mResultReader;
+
+ /**
+ * output stream to provide new addresses to decode to the command line
+ * process
+ */
+ private BufferedOutputStream mAddressWriter;
+
+ /**
+ * Returns the instance of a Addr2Line process for the specified library.
+ * <br>The library should be in a format that makes<br>
+ * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
+ *
+ * @param library the library in which to look for addresses.
+ * @return a new Addr2Line object representing a started process, ready to
+ * be queried for addresses. If any error happened when launching a
+ * new process, <code>null</code> will be returned.
+ */
+ public static Addr2Line getProcess(final String library) {
+ // synchronize around the hashmap object
+ if (library != null) {
+ synchronized (sProcessCache) {
+ // look for an existing process
+ Addr2Line process = sProcessCache.get(library);
+
+ // if we don't find one, we create it
+ if (process == null) {
+ process = new Addr2Line(library);
+
+ // then we start it
+ boolean status = process.start();
+
+ if (status) {
+ // if starting the process worked, then we add it to the
+ // list.
+ sProcessCache.put(library, process);
+ } else {
+ // otherwise we just drop the object, to return null
+ process = null;
+ }
+ }
+ // return the process
+ return process;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Construct the object with a library name.
+ * <br>The library should be in a format that makes<br>
+ * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
+ *
+ * @param library the library in which to look for address.
+ */
+ private Addr2Line(final String library) {
+ mLibrary = library;
+ }
+
+ /**
+ * Starts the command line process.
+ *
+ * @return true if the process was started, false if it failed to start, or
+ * if there was any other errors.
+ */
+ private boolean start() {
+ // because this is only called from getProcess() we know we don't need
+ // to synchronize this code.
+
+ // get the output directory.
+ String symbols = DdmUiPreferences.getSymbolDirectory();
+
+ // build the command line
+ String[] command = new String[5];
+ command[0] = DdmUiPreferences.getAddr2Line();
+ command[1] = "-C";
+ command[2] = "-f";
+ command[3] = "-e";
+ command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so");
+
+ try {
+ // attempt to start the process
+ mProcess = Runtime.getRuntime().exec(command);
+
+ if (mProcess != null) {
+ // get the result reader
+ InputStreamReader is = new InputStreamReader(mProcess
+ .getInputStream());
+ mResultReader = new BufferedReader(is);
+
+ // get the outstream to write the addresses
+ mAddressWriter = new BufferedOutputStream(mProcess
+ .getOutputStream());
+
+ // check our streams are here
+ if (mResultReader == null || mAddressWriter == null) {
+ // not here? stop the process and return false;
+ mProcess.destroy();
+ mProcess = null;
+ return false;
+ }
+
+ // return a success
+ return true;
+ }
+
+ } catch (IOException e) {
+ // log the error
+ String msg = String.format(
+ "Error while trying to start %1$s process for library %2$s",
+ DdmUiPreferences.getAddr2Line(), mLibrary);
+ Log.e("ddm-Addr2Line", msg);
+
+ // drop the process just in case
+ if (mProcess != null) {
+ mProcess.destroy();
+ mProcess = null;
+ }
+ }
+
+ // we can be here either cause the allocation of mProcess failed, or we
+ // caught an exception
+ return false;
+ }
+
+ /**
+ * Stops the command line process.
+ */
+ public void stop() {
+ synchronized (sProcessCache) {
+ if (mProcess != null) {
+ // remove the process from the list
+ sProcessCache.remove(mLibrary);
+
+ // then stops the process
+ mProcess.destroy();
+
+ // set the reference to null.
+ // this allows to make sure another thread calling getAddress()
+ // will not query a stopped thread
+ mProcess = null;
+ }
+ }
+ }
+
+ /**
+ * Stops all current running processes.
+ */
+ public static void stopAll() {
+ // because of concurrent access (and our use of HashMap.values()), we
+ // can't rely on the synchronized inside stop(). We need to put one
+ // around the whole loop.
+ synchronized (sProcessCache) {
+ // just a basic loop on all the values in the hashmap and call to
+ // stop();
+ Collection<Addr2Line> col = sProcessCache.values();
+ for (Addr2Line a2l : col) {
+ a2l.stop();
+ }
+ }
+ }
+
+ /**
+ * Looks up an address and returns method name, source file name, and line
+ * number.
+ *
+ * @param addr the address to look up
+ * @return a BacktraceInfo object containing the method/filename/linenumber
+ * or null if the process we stopped before the query could be
+ * processed, or if an IO exception happened.
+ */
+ public NativeStackCallInfo getAddress(long addr) {
+ // even though we don't access the hashmap object, we need to
+ // synchronized on it to prevent
+ // another thread from stopping the process we're going to query.
+ synchronized (sProcessCache) {
+ // check the process is still alive/allocated
+ if (mProcess != null) {
+ // prepare to the write the address to the output buffer.
+
+ // first, conversion to a string containing the hex value.
+ String tmp = Long.toString(addr, 16);
+
+ try {
+ // write the address to the buffer
+ mAddressWriter.write(tmp.getBytes());
+
+ // add CR-LF
+ mAddressWriter.write(sCrLf);
+
+ // flush it all.
+ mAddressWriter.flush();
+
+ // read the result. We need to read 2 lines
+ String method = mResultReader.readLine();
+ String source = mResultReader.readLine();
+
+ // make the backtrace object and return it
+ if (method != null && source != null) {
+ return new NativeStackCallInfo(mLibrary, method, source);
+ }
+ } catch (IOException e) {
+ // log the error
+ Log.e("ddms",
+ "Error while trying to get information for addr: "
+ + tmp + " in library: " + mLibrary);
+ // we'll return null later
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java
new file mode 100644
index 0000000..45d45ff
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AllocationInfo;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Base class for our information panels.
+ */
+public class AllocationPanel extends TablePanel {
+
+ private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$
+ private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$
+
+ private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$
+
+ private static final String PREFS_STACK_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$
+
+ private Composite mAllocationBase;
+ private Table mAllocationTable;
+ private TableViewer mAllocationViewer;
+
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+ private Button mEnableButton;
+ private Button mRequestButton;
+
+ /**
+ * Content Provider to display the allocations of a client.
+ * Expected input is a {@link Client} object, elements used in the table are of type
+ * {@link AllocationInfo}.
+ */
+ private static class AllocationContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations();
+ if (allocs != null) {
+ return allocs;
+ }
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be
+ * of type {@link AllocationInfo}.
+ */
+ private static class AllocationLabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof AllocationInfo) {
+ AllocationInfo alloc = (AllocationInfo)element;
+ switch (columnIndex) {
+ case 0:
+ return Integer.toString(alloc.getSize());
+ case 1:
+ return alloc.getAllocatedClass();
+ case 2:
+ return Short.toString(alloc.getThreadId());
+ case 3:
+ StackTraceElement[] traces = alloc.getStackTrace();
+ if (traces.length > 0) {
+ return traces[0].getClassName();
+ }
+ break;
+ case 4:
+ traces = alloc.getStackTrace();
+ if (traces.length > 0) {
+ return traces[0].getMethodName();
+ }
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // base composite for selected client with enabled thread update.
+ mAllocationBase = new Composite(parent, SWT.NONE);
+ mAllocationBase.setLayout(new FormLayout());
+
+ // table above the sash
+ Composite topParent = new Composite(mAllocationBase, SWT.NONE);
+ topParent.setLayout(new GridLayout(2, false));
+
+ mEnableButton = new Button(topParent, SWT.PUSH);
+ mEnableButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Client current = getCurrentClient();
+ int status = current.getClientData().getAllocationStatus();
+ if (status == ClientData.ALLOCATION_TRACKING_ON) {
+ current.enableAllocationTracker(false);
+ } else {
+ current.enableAllocationTracker(true);
+ }
+ current.requestAllocationStatus();
+ }
+ });
+
+ mRequestButton = new Button(topParent, SWT.PUSH);
+ mRequestButton.setText("Get Allocations");
+ mRequestButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ getCurrentClient().requestAllocationDetails();
+ }
+ });
+
+ setUpButtons(false /* enabled */, ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */);
+
+ mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION);
+ GridData gridData;
+ mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH));
+ gridData.horizontalSpan = 2;
+ mAllocationTable.setHeaderVisible(true);
+ mAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocation Size",
+ SWT.RIGHT,
+ "888", //$NON-NLS-1$
+ PREFS_ALLOC_COL_SIZE, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated Class",
+ SWT.LEFT,
+ "Allocated Class", //$NON-NLS-1$
+ PREFS_ALLOC_COL_CLASS, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Thread Id",
+ SWT.LEFT,
+ "999", //$NON-NLS-1$
+ PREFS_ALLOC_COL_THREAD, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_CLASS, store);
+
+ TableHelper.createTableColumn(
+ mAllocationTable,
+ "Allocated in",
+ SWT.LEFT,
+ "utime", //$NON-NLS-1$
+ PREFS_ALLOC_COL_TRACE_METHOD, store);
+
+ mAllocationViewer = new TableViewer(mAllocationTable);
+ mAllocationViewer.setContentProvider(new AllocationContentProvider());
+ mAllocationViewer.setLabelProvider(new AllocationLabelProvider());
+
+ mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection());
+ updateAllocationStackTrace(selectedAlloc);
+ }
+ });
+
+ // the separating sash
+ final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL);
+ Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+ sash.setBackground(darkGray);
+
+ // the UI below the sash
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase,
+ PREFS_STACK_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ store);
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ topParent.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_ALLOC_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = mAllocationBase.getClientArea();
+ int bottom = panelRect.height - sashRect.height - 100;
+ e.y = Math.max(Math.min(e.y, bottom), 100);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ store.setValue(PREFS_ALLOC_SASH, e.y);
+ mAllocationBase.layout();
+ }
+ }
+ });
+
+ return mAllocationBase;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mAllocationTable.setFocus();
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) {
+ try {
+ mAllocationTable.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ mAllocationViewer.refresh();
+ updateAllocationStackCall();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) {
+ try {
+ mAllocationTable.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ setUpButtons(true, client.getClientData().getAllocationStatus());
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mAllocationTable.isDisposed()) {
+ return;
+ }
+
+ Client client = getCurrentClient();
+
+ mStackTracePanel.setCurrentClient(client);
+ mStackTracePanel.setViewerInput(null); // always empty on client selection change.
+
+ if (client != null) {
+ setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus());
+ } else {
+ setUpButtons(false /* enabled */,
+ ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */);
+ }
+
+ mAllocationViewer.setInput(client);
+ }
+
+ /**
+ * Updates the stack call of the currently selected thread.
+ * <p/>
+ * This <b>must</b> be called from the UI thread.
+ */
+ private void updateAllocationStackCall() {
+ Client client = getCurrentClient();
+ if (client != null) {
+ // get the current selection in the ThreadTable
+ AllocationInfo selectedAlloc = getAllocationSelection(null);
+
+ if (selectedAlloc != null) {
+ updateAllocationStackTrace(selectedAlloc);
+ } else {
+ updateAllocationStackTrace(null);
+ }
+ }
+ }
+
+ /**
+ * updates the stackcall of the specified allocation. If <code>null</code> the UI is emptied
+ * of current data.
+ * @param thread
+ */
+ private void updateAllocationStackTrace(AllocationInfo alloc) {
+ mStackTracePanel.setViewerInput(alloc);
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mAllocationTable);
+ addTableToFocusListener(mStackTraceTable);
+ }
+
+ /**
+ * Returns the current allocation selection or <code>null</code> if none is found.
+ * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this
+ * selection is returned, otherwise, the <code>ISelection</code> returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or <code>null</code>
+ */
+ private AllocationInfo getAllocationSelection(ISelection selection) {
+ if (selection == null) {
+ selection = mAllocationViewer.getSelection();
+ }
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof AllocationInfo) {
+ return (AllocationInfo)object;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ *
+ * @param enabled
+ * @param trackingStatus
+ */
+ private void setUpButtons(boolean enabled, int trackingStatus) {
+ if (enabled) {
+ switch (trackingStatus) {
+ case ClientData.ALLOCATION_TRACKING_UNKNOWN:
+ mEnableButton.setText("?");
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ break;
+ case ClientData.ALLOCATION_TRACKING_OFF:
+ mEnableButton.setText("Start Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(false);
+ break;
+ case ClientData.ALLOCATION_TRACKING_ON:
+ mEnableButton.setText("Stop Tracking");
+ mEnableButton.setEnabled(true);
+ mRequestButton.setEnabled(true);
+ break;
+ }
+ } else {
+ mEnableButton.setEnabled(false);
+ mRequestButton.setEnabled(false);
+ mEnableButton.setText("Start Tracking");
+ }
+ }
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java
new file mode 100644
index 0000000..0ed4c95
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+/**
+ * base background thread class. The class provides a synchronous quit method
+ * which sets a quitting flag to true. Inheriting classes should regularly test
+ * this flag with <code>isQuitting()</code> and should finish if the flag is
+ * true.
+ */
+public abstract class BackgroundThread extends Thread {
+ private boolean mQuit = false;
+
+ /**
+ * Tell the thread to exit. This is usually called from the UI thread. The
+ * call is synchronous and will only return once the thread has terminated
+ * itself.
+ */
+ public final void quit() {
+ mQuit = true;
+ Log.d("ddms", "Waiting for BackgroundThread to quit");
+ try {
+ this.join();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ /** returns if the thread was asked to quit. */
+ protected final boolean isQuitting() {
+ return mQuit;
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java
new file mode 100644
index 0000000..3e66ea5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.HeapSegment;
+import com.android.ddmlib.ClientData.HeapData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Base Panel for heap panels.
+ */
+public abstract class BaseHeapPanel extends TablePanel {
+
+ /** store the processed heap segment, so that we don't recompute Image for nothing */
+ protected byte[] mProcessedHeapData;
+ private Map<Integer, ArrayList<HeapSegmentElement>> mHeapMap;
+
+ /**
+ * Serialize the heap data into an array. The resulting array is available through
+ * <code>getSerializedData()</code>.
+ * @param heapData The heap data to serialize
+ * @return true if the data changed.
+ */
+ protected boolean serializeHeapData(HeapData heapData) {
+ Collection<HeapSegment> heapSegments;
+
+ // Atomically get and clear the heap data.
+ synchronized (heapData) {
+ // get the segments
+ heapSegments = heapData.getHeapSegments();
+
+
+ if (heapSegments != null) {
+ // if they are not null, we never processed them.
+ // Before we process then, we drop them from the HeapData
+ heapData.clearHeapData();
+
+ // process them into a linear byte[]
+ doSerializeHeapData(heapSegments);
+ heapData.setProcessedHeapData(mProcessedHeapData);
+ heapData.setProcessedHeapMap(mHeapMap);
+
+ } else {
+ // the heap segments are null. Let see if the heapData contains a
+ // list that is already processed.
+
+ byte[] pixData = heapData.getProcessedHeapData();
+
+ // and compare it to the one we currently have in the panel.
+ if (pixData == mProcessedHeapData) {
+ // looks like its the same
+ return false;
+ } else {
+ mProcessedHeapData = pixData;
+ }
+
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+ heapData.getProcessedHeapMap();
+ mHeapMap = heapMap;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the serialized heap data
+ */
+ protected byte[] getSerializedData() {
+ return mProcessedHeapData;
+ }
+
+ /**
+ * Processes and serialize the heapData.
+ * <p/>
+ * The resulting serialized array is {@link #mProcessedHeapData}.
+ * <p/>
+ * the resulting map is {@link #mHeapMap}.
+ * @param heapData the collection of {@link HeapSegment} that forms the heap data.
+ */
+ private void doSerializeHeapData(Collection<HeapSegment> heapData) {
+ mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>();
+
+ Iterator<HeapSegment> iterator;
+ ByteArrayOutputStream out;
+
+ out = new ByteArrayOutputStream(4 * 1024);
+
+ iterator = heapData.iterator();
+ while (iterator.hasNext()) {
+ HeapSegment hs = iterator.next();
+
+ HeapSegmentElement e = null;
+ while (true) {
+ int v;
+
+ e = hs.getNextElement(null);
+ if (e == null) {
+ break;
+ }
+
+ if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) {
+ v = 1;
+ } else {
+ v = e.getKind() + 2;
+ }
+
+ // put the element in the map
+ ArrayList<HeapSegmentElement> elementList = mHeapMap.get(v);
+ if (elementList == null) {
+ elementList = new ArrayList<HeapSegmentElement>();
+ mHeapMap.put(v, elementList);
+ }
+ elementList.add(e);
+
+
+ int len = e.getLength() / 8;
+ while (len > 0) {
+ out.write(v);
+ --len;
+ }
+ }
+ }
+ mProcessedHeapData = out.toByteArray();
+
+ // sort the segment element in the heap info.
+ Collection<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values();
+ for (ArrayList<HeapSegmentElement> elementList : elementLists) {
+ Collections.sort(elementList);
+ }
+ }
+
+ /**
+ * Creates a linear image of the heap data.
+ * @param pixData
+ * @param h
+ * @param palette
+ * @return
+ */
+ protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) {
+ int w = pixData.length / h;
+ if (pixData.length % h != 0) {
+ w++;
+ }
+
+ // Create the heap image.
+ ImageData id = new ImageData(w, h, 8, palette);
+
+ int x = 0;
+ int y = 0;
+ for (byte b : pixData) {
+ if (b >= 0) {
+ id.setPixel(x, y, b);
+ }
+
+ y++;
+ if (y >= h) {
+ y = 0;
+ x++;
+ }
+ }
+
+ return id;
+ }
+
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java
new file mode 100644
index 0000000..a711933
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+public abstract class ClientDisplayPanel extends SelectionDependentPanel
+ implements IClientChangeListener {
+
+ @Override
+ protected void postCreation() {
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ public void dispose() {
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java
new file mode 100644
index 0000000..f832a4e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+/**
+ * Preference entry point for ddmuilib. Allows the lib to access a preference
+ * store (org.eclipse.jface.preference.IPreferenceStore) defined by the
+ * application that includes the lib.
+ */
+public final class DdmUiPreferences {
+
+ public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds
+
+ private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL;
+
+ private static IPreferenceStore mStore;
+
+ private static String sSymbolLocation =""; //$NON-NLS-1$
+ private static String sAddr2LineLocation =""; //$NON-NLS-1$
+ private static String sTraceviewLocation =""; //$NON-NLS-1$
+
+ public static void setStore(IPreferenceStore store) {
+ mStore = store;
+ }
+
+ public static IPreferenceStore getStore() {
+ return mStore;
+ }
+
+ public static int getThreadRefreshInterval() {
+ return sThreadRefreshInterval;
+ }
+
+ public static void setThreadRefreshInterval(int port) {
+ sThreadRefreshInterval = port;
+ }
+
+ static String getSymbolDirectory() {
+ return sSymbolLocation;
+ }
+
+ public static void setSymbolsLocation(String location) {
+ sSymbolLocation = location;
+ }
+
+ static String getAddr2Line() {
+ return sAddr2LineLocation;
+ }
+
+ public static void setAddr2LineLocation(String location) {
+ sAddr2LineLocation = location;
+ }
+
+ public static String getTraceview() {
+ return sTraceviewLocation;
+ }
+
+ public static void setTraceviewLocation(String location) {
+ sTraceviewLocation = location;
+ }
+
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java
new file mode 100644
index 0000000..81b757e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Device.DeviceState;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+
+/**
+ * A display of both the devices and their clients.
+ */
+public final class DevicePanel extends Panel implements IDebugBridgeChangeListener,
+ IDeviceChangeListener, IClientChangeListener {
+
+ private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$
+
+ private final static int DEVICE_COL_SERIAL = 0;
+ private final static int DEVICE_COL_STATE = 1;
+ // col 2, 3 not used.
+ private final static int DEVICE_COL_BUILD = 4;
+
+ private final static int CLIENT_COL_NAME = 0;
+ private final static int CLIENT_COL_PID = 1;
+ private final static int CLIENT_COL_THREAD = 2;
+ private final static int CLIENT_COL_HEAP = 3;
+ private final static int CLIENT_COL_PORT = 4;
+
+ public final static int ICON_WIDTH = 16;
+ public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$
+ public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$
+ public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$
+ public final static String ICON_GC = "gc.png"; //$NON-NLS-1$
+
+ private Device mCurrentDevice;
+ private Client mCurrentClient;
+
+ private Tree mTree;
+ private TreeViewer mTreeViewer;
+
+ private Image mDeviceImage;
+ private Image mEmulatorImage;
+
+ private Image mThreadImage;
+ private Image mHeapImage;
+ private Image mWaitingImage;
+ private Image mDebuggerImage;
+ private Image mDebugErrorImage;
+
+ private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>();
+
+ private final ArrayList<Device> mDevicesToExpand = new ArrayList<Device>();
+
+ private IImageLoader mLoader;
+
+ private boolean mAdvancedPortSupport;
+
+ /**
+ * A Content provider for the {@link TreeViewer}.
+ * <p/>
+ * The input is a {@link AndroidDebugBridge}. First level elements are {@link Device} objects,
+ * and second level elements are {@link Client} object.
+ */
+ private class ContentProvider implements ITreeContentProvider {
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof Device) {
+ return ((Device)parentElement).getClients();
+ }
+ return new Object[0];
+ }
+
+ public Object getParent(Object element) {
+ if (element instanceof Client) {
+ return ((Client)element).getDevice();
+ }
+ return null;
+ }
+
+ public boolean hasChildren(Object element) {
+ if (element instanceof Device) {
+ return ((Device)element).hasClients();
+ }
+
+ // Clients never have children.
+ return false;
+ }
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof AndroidDebugBridge) {
+ return ((AndroidDebugBridge)inputElement).getDevices();
+ }
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides
+ * labels and images for {@link Device} and {@link Client} objects.
+ */
+ private class LabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == DEVICE_COL_SERIAL && element instanceof Device) {
+ Device device = (Device)element;
+ if (device.isEmulator()) {
+ return mEmulatorImage;
+ }
+
+ return mDeviceImage;
+ } else if (element instanceof Client) {
+ Client client = (Client)element;
+ ClientData cd = client.getClientData();
+
+ switch (columnIndex) {
+ case CLIENT_COL_NAME:
+ switch (cd.getDebuggerConnectionStatus()) {
+ case ClientData.DEBUGGER_DEFAULT:
+ return null;
+ case ClientData.DEBUGGER_WAITING:
+ return mWaitingImage;
+ case ClientData.DEBUGGER_ATTACHED:
+ return mDebuggerImage;
+ case ClientData.DEBUGGER_ERROR:
+ return mDebugErrorImage;
+ }
+ return null;
+ case CLIENT_COL_THREAD:
+ if (client.isThreadUpdateEnabled()) {
+ return mThreadImage;
+ }
+ return null;
+ case CLIENT_COL_HEAP:
+ if (client.isHeapUpdateEnabled()) {
+ return mHeapImage;
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof Device) {
+ Device device = (Device)element;
+ switch (columnIndex) {
+ case DEVICE_COL_SERIAL:
+ return device.getSerialNumber();
+ case DEVICE_COL_STATE:
+ return getStateString(device);
+ case DEVICE_COL_BUILD: {
+ String version = device.getProperty(Device.PROP_BUILD_VERSION);
+ if (version != null) {
+ String debuggable = device.getProperty(Device.PROP_DEBUGGABLE);
+ if (device.isEmulator()) {
+ String avdName = device.getAvdName();
+ if (avdName == null) {
+ avdName = "?"; // the device is probably not online yet, so
+ // we don't know its AVD name just yet.
+ }
+ if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+ return String.format("%1$s [%2$s, debug]", avdName,
+ version);
+ } else {
+ return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$
+ }
+ } else {
+ if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+ return String.format("%1$s, debug", version);
+ } else {
+ return String.format("%1$s", version); //$NON-NLS-1$
+ }
+ }
+ } else {
+ return "unknown";
+ }
+ }
+ }
+ } else if (element instanceof Client) {
+ Client client = (Client)element;
+ ClientData cd = client.getClientData();
+
+ switch (columnIndex) {
+ case CLIENT_COL_NAME:
+ String name = cd.getClientDescription();
+ if (name != null) {
+ return name;
+ }
+ return "?";
+ case CLIENT_COL_PID:
+ return Integer.toString(cd.getPid());
+ case CLIENT_COL_PORT:
+ if (mAdvancedPortSupport) {
+ int port = client.getDebuggerListenPort();
+ String portString = "?";
+ if (port != 0) {
+ portString = Integer.toString(port);
+ }
+ if (client.isSelectedClient()) {
+ return String.format("%1$s / %2$d", portString, //$NON-NLS-1$
+ DdmPreferences.getSelectedDebugPort());
+ }
+
+ return portString;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deals
+ * with {@link Device} and {@link Client} selection changes coming from the ui.
+ */
+ public interface IUiSelectionListener {
+ /**
+ * Sent when a new {@link Device} and {@link Client} are selected.
+ * @param selectedDevice the selected device. If null, no devices are selected.
+ * @param selectedClient The selected client. If null, no clients are selected.
+ */
+ public void selectionChanged(Device selectedDevice, Client selectedClient);
+ }
+
+ /**
+ * Creates the {@link DevicePanel} object.
+ * @param loader
+ * @param advancedPortSupport if true the device panel will add support for selected client port
+ * and display the ports in the ui.
+ */
+ public DevicePanel(IImageLoader loader, boolean advancedPortSupport) {
+ mLoader = loader;
+ mAdvancedPortSupport = advancedPortSupport;
+ }
+
+ public void addSelectionListener(IUiSelectionListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeSelectionListener(IUiSelectionListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ loadImages(parent.getDisplay(), mLoader);
+
+ parent.setLayout(new FillLayout());
+
+ // create the tree and its column
+ mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION);
+ mTree.setHeaderVisible(true);
+ mTree.setLinesVisible(true);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+ "com.android.home", //$NON-NLS-1$
+ PREFS_COL_NAME_SERIAL, store);
+ TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+ "Offline", //$NON-NLS-1$
+ PREFS_COL_PID_STATE, store);
+
+ TreeColumn col = new TreeColumn(mTree, SWT.NONE);
+ col.setWidth(ICON_WIDTH + 8);
+ col.setResizable(false);
+ col = new TreeColumn(mTree, SWT.NONE);
+ col.setWidth(ICON_WIDTH + 8);
+ col.setResizable(false);
+
+ TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+ "9999-9999", //$NON-NLS-1$
+ PREFS_COL_PORT_BUILD, store);
+
+ // create the tree viewer
+ mTreeViewer = new TreeViewer(mTree);
+
+ // make the device auto expanded.
+ mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+ // set up the content and label providers.
+ mTreeViewer.setContentProvider(new ContentProvider());
+ mTreeViewer.setLabelProvider(new LabelProvider());
+
+ mTree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ notifyListeners();
+ }
+ });
+
+ return mTree;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTree.setFocus();
+ }
+
+ @Override
+ protected void postCreation() {
+ // ask for notification of changes in AndroidDebugBridge (a new one is created when
+ // adb is restarted from a different location), Device and Client objects.
+ AndroidDebugBridge.addDebugBridgeChangeListener(this);
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ public void dispose() {
+ AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+
+ /**
+ * Returns the selected {@link Client}. May be null.
+ */
+ public Client getSelectedClient() {
+ return mCurrentClient;
+ }
+
+ /**
+ * Returns the selected {@link Device}. If a {@link Client} is selected, it returns the
+ * Device object containing the client.
+ */
+ public Device getSelectedDevice() {
+ return mCurrentDevice;
+ }
+
+ /**
+ * Kills the selected {@link Client} by sending its VM a halt command.
+ */
+ public void killSelectedClient() {
+ if (mCurrentClient != null) {
+ Client client = mCurrentClient;
+
+ // reset the selection to the device.
+ TreePath treePath = new TreePath(new Object[] { mCurrentDevice });
+ TreeSelection treeSelection = new TreeSelection(treePath);
+ mTreeViewer.setSelection(treeSelection);
+
+ client.kill();
+ }
+ }
+
+ /**
+ * Forces a GC on the selected {@link Client}.
+ */
+ public void forceGcOnSelectedClient() {
+ if (mCurrentClient != null) {
+ mCurrentClient.executeGarbageCollector();
+ }
+ }
+
+ public void setEnabledHeapOnSelectedClient(boolean enable) {
+ if (mCurrentClient != null) {
+ mCurrentClient.setHeapUpdateEnabled(enable);
+ }
+ }
+
+ public void setEnabledThreadOnSelectedClient(boolean enable) {
+ if (mCurrentClient != null) {
+ mCurrentClient.setThreadUpdateEnabled(enable);
+ }
+ }
+
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is started.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ *
+ * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge)
+ */
+ public void bridgeChanged(final AndroidDebugBridge bridge) {
+ if (mTree.isDisposed() == false) {
+ exec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // set up the data source.
+ mTreeViewer.setInput(bridge);
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ // all current devices are obsolete
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.clear();
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(Device)
+ */
+ public void deviceConnected(Device device) {
+ exec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // refresh all
+ mTreeViewer.refresh();
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+
+ // if it doesn't have clients yet, it'll need to be manually expanded when it gets them.
+ if (device.hasClients() == false) {
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.add(device);
+ }
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(Device)
+ */
+ public void deviceDisconnected(Device device) {
+ deviceConnected(device);
+
+ // just in case, we remove it from the list of devices to expand.
+ synchronized (mDevicesToExpand) {
+ mDevicesToExpand.remove(device);
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask indicating what changed.
+ *
+ * @see IDeviceChangeListener#deviceChanged(Device)
+ */
+ public void deviceChanged(final Device device, int changeMask) {
+ boolean expand = false;
+ synchronized (mDevicesToExpand) {
+ int index = mDevicesToExpand.indexOf(device);
+ if (device.hasClients() && index != -1) {
+ mDevicesToExpand.remove(index);
+ expand = true;
+ }
+ }
+
+ final boolean finalExpand = expand;
+
+ exec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // look if the current device is selected. This is done in case the current
+ // client of this particular device was killed. In this case, we'll need to
+ // manually reselect the device.
+
+ Device selectedDevice = getSelectedDevice();
+
+ // refresh the device
+ mTreeViewer.refresh(device);
+
+ // if the selected device was the changed device and the new selection is
+ // empty, we reselect the device.
+ if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) {
+ mTreeViewer.setSelection(new TreeSelection(new TreePath(
+ new Object[] { device })));
+ }
+
+ // notify the listener of a possible selection change.
+ notifyListeners();
+
+ if (finalExpand) {
+ mTreeViewer.setExpandedState(device, true);
+ }
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO},
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, final int changeMask) {
+ exec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // refresh the client
+ mTreeViewer.refresh(client);
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) ==
+ Client.CHANGE_DEBUGGER_INTEREST &&
+ client.getClientData().getDebuggerConnectionStatus() ==
+ ClientData.DEBUGGER_WAITING) {
+ // make sure the device is expanded. Normally the setSelection below
+ // will auto expand, but the children of device may not already exist
+ // at this time. Forcing an expand will make the TreeViewer create them.
+ Device device = client.getDevice();
+ if (mTreeViewer.getExpandedState(device) == false) {
+ mTreeViewer.setExpandedState(device, true);
+ }
+
+ // create and set the selection
+ TreePath treePath = new TreePath(new Object[] { device, client});
+ TreeSelection treeSelection = new TreeSelection(treePath);
+ mTreeViewer.setSelection(treeSelection);
+
+ if (mAdvancedPortSupport) {
+ client.setAsSelectedClient();
+ }
+
+ // notify the listener of a possible selection change.
+ notifyListeners(device, client);
+ }
+ } else {
+ // tree is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+ AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+ }
+ }
+ });
+ }
+
+ private void loadImages(Display display, IImageLoader loader) {
+ if (mDeviceImage == null) {
+ mDeviceImage = ImageHelper.loadImage(loader, display, "device.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mEmulatorImage == null) {
+ mEmulatorImage = ImageHelper.loadImage(loader, display,
+ "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mThreadImage == null) {
+ mThreadImage = ImageHelper.loadImage(loader, display, ICON_THREAD,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_YELLOW));
+ }
+ if (mHeapImage == null) {
+ mHeapImage = ImageHelper.loadImage(loader, display, ICON_HEAP,
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+ if (mWaitingImage == null) {
+ mWaitingImage = ImageHelper.loadImage(loader, display,
+ "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mDebuggerImage == null) {
+ mDebuggerImage = ImageHelper.loadImage(loader, display,
+ "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_GREEN));
+ }
+ if (mDebugErrorImage == null) {
+ mDebugErrorImage = ImageHelper.loadImage(loader, display,
+ "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ }
+
+ /**
+ * Returns a display string representing the state of the device.
+ * @param d the device
+ */
+ private static String getStateString(Device d) {
+ DeviceState deviceState = d.getState();
+ if (deviceState == DeviceState.ONLINE) {
+ return "Online";
+ } else if (deviceState == DeviceState.OFFLINE) {
+ return "Offline";
+ } else if (deviceState == DeviceState.BOOTLOADER) {
+ return "Bootloader";
+ }
+
+ return "??";
+ }
+
+ /**
+ * Executes the {@link Runnable} in the UI thread.
+ * @param runnable the runnable to execute.
+ */
+ private void exec(Runnable runnable) {
+ try {
+ Display display = mTree.getDisplay();
+ display.asyncExec(runnable);
+ } catch (SWTException e) {
+ // tree is disposed, we need to do something. lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ AndroidDebugBridge.removeClientChangeListener(this);
+ }
+ }
+
+ private void notifyListeners() {
+ // get the selection
+ TreeItem[] items = mTree.getSelection();
+
+ Client client = null;
+ Device device = null;
+
+ if (items.length == 1) {
+ Object object = items[0].getData();
+ if (object instanceof Client) {
+ client = (Client)object;
+ device = client.getDevice();
+ } else if (object instanceof Device) {
+ device = (Device)object;
+ }
+ }
+
+ notifyListeners(device, client);
+ }
+
+ private void notifyListeners(Device selectedDevice, Client selectedClient) {
+ if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) {
+ mCurrentDevice = selectedDevice;
+ mCurrentClient = selectedClient;
+
+ for (IUiSelectionListener listener : mListeners) {
+ // notify the listener with a try/catch-all to make sure this thread won't die
+ // because of an uncaught exception before all the listeners were notified.
+ try {
+ listener.selectionChanged(selectedDevice, selectedClient);
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java
new file mode 100644
index 0000000..5583760
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java
@@ -0,0 +1,1454 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.EmulatorConsole;
+import com.android.ddmlib.EmulatorConsole.GsmMode;
+import com.android.ddmlib.EmulatorConsole.GsmStatus;
+import com.android.ddmlib.EmulatorConsole.NetworkStatus;
+import com.android.ddmuilib.location.CoordinateControls;
+import com.android.ddmuilib.location.GpxParser;
+import com.android.ddmuilib.location.KmlParser;
+import com.android.ddmuilib.location.TrackContentProvider;
+import com.android.ddmuilib.location.TrackLabelProvider;
+import com.android.ddmuilib.location.TrackPoint;
+import com.android.ddmuilib.location.WayPoint;
+import com.android.ddmuilib.location.WayPointContentProvider;
+import com.android.ddmuilib.location.WayPointLabelProvider;
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Panel to control the emulator using EmulatorConsole objects.
+ */
+public class EmulatorControlPanel extends SelectionDependentPanel {
+
+ // default location: Patio outside Charlie's
+ private final static double DEFAULT_LONGITUDE = -122.084095;
+ private final static double DEFAULT_LATITUDE = 37.422006;
+
+ private final static String SPEED_FORMAT = "Speed: %1$dX";
+
+
+ /**
+ * Map between the display gsm mode and the internal tag used by the display.
+ */
+ private final static String[][] GSM_MODES = new String[][] {
+ { "unregistered", GsmMode.UNREGISTERED.getTag() },
+ { "home", GsmMode.HOME.getTag() },
+ { "roaming", GsmMode.ROAMING.getTag() },
+ { "searching", GsmMode.SEARCHING.getTag() },
+ { "denied", GsmMode.DENIED.getTag() },
+ };
+
+ private final static String[] NETWORK_SPEEDS = new String[] {
+ "Full",
+ "GSM",
+ "HSCSD",
+ "GPRS",
+ "EDGE",
+ "UMTS",
+ "HSDPA",
+ };
+
+ private final static String[] NETWORK_LATENCIES = new String[] {
+ "None",
+ "GPRS",
+ "EDGE",
+ "UMTS",
+ };
+
+ private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 };
+
+ private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$
+ private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$
+ private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$
+
+ private IImageLoader mImageLoader;
+
+ private EmulatorConsole mEmulatorConsole;
+
+ private Composite mParent;
+
+ private Label mVoiceLabel;
+ private Combo mVoiceMode;
+ private Label mDataLabel;
+ private Combo mDataMode;
+ private Label mSpeedLabel;
+ private Combo mNetworkSpeed;
+ private Label mLatencyLabel;
+ private Combo mNetworkLatency;
+
+ private Label mNumberLabel;
+ private Text mPhoneNumber;
+
+ private Button mVoiceButton;
+ private Button mSmsButton;
+
+ private Label mMessageLabel;
+ private Text mSmsMessage;
+
+ private Button mCallButton;
+ private Button mCancelButton;
+
+ private TabFolder mLocationFolders;
+
+ private Button mDecimalButton;
+ private Button mSexagesimalButton;
+ private CoordinateControls mLongitudeControls;
+ private CoordinateControls mLatitudeControls;
+ private Button mGpxUploadButton;
+ private Table mGpxWayPointTable;
+ private Table mGpxTrackTable;
+ private Button mKmlUploadButton;
+ private Table mKmlWayPointTable;
+
+ private Button mPlayGpxButton;
+ private Button mGpxBackwardButton;
+ private Button mGpxForwardButton;
+ private Button mGpxSpeedButton;
+ private Button mPlayKmlButton;
+ private Button mKmlBackwardButton;
+ private Button mKmlForwardButton;
+ private Button mKmlSpeedButton;
+
+ private Image mPlayImage;
+ private Image mPauseImage;
+
+ private Thread mPlayingThread;
+ private boolean mPlayingTrack;
+ private int mPlayDirection = 1;
+ private int mSpeed;
+ private int mSpeedIndex;
+
+ private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Button b = (Button)e.getSource();
+ if (b.getSelection() == false) {
+ // basically the button was unselected, which we don't allow.
+ // so we reselect it.
+ b.setSelection(true);
+ return;
+ }
+
+ // now handle selection change.
+ if (b == mGpxForwardButton || b == mKmlForwardButton) {
+ mGpxBackwardButton.setSelection(false);
+ mGpxForwardButton.setSelection(true);
+ mKmlBackwardButton.setSelection(false);
+ mKmlForwardButton.setSelection(true);
+ mPlayDirection = 1;
+
+ } else {
+ mGpxBackwardButton.setSelection(true);
+ mGpxForwardButton.setSelection(false);
+ mKmlBackwardButton.setSelection(true);
+ mKmlForwardButton.setSelection(false);
+ mPlayDirection = -1;
+ }
+ }
+ };
+
+ private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mGpxPlayControls.pack();
+ mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mKmlPlayControls.pack();
+
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ };
+ private Composite mKmlPlayControls;
+ private Composite mGpxPlayControls;
+
+
+ public EmulatorControlPanel(IImageLoader imageLoader) {
+ mImageLoader = imageLoader;
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ handleNewDevice(getCurrentDevice());
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+
+ final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL);
+ scollingParent.setExpandVertical(true);
+ scollingParent.setExpandHorizontal(true);
+ scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ final Composite top = new Composite(scollingParent, SWT.NONE);
+ scollingParent.setContent(top);
+ top.setLayout(new GridLayout(1, false));
+
+ // set the resize for the scrolling to work (why isn't that done automatically?!?)
+ scollingParent.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = scollingParent.getClientArea();
+ scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+ }
+ });
+
+ createRadioControls(top);
+
+ createCallControls(top);
+
+ createLocationControls(top);
+
+ doEnable(false);
+
+ top.layout();
+ Rectangle r = scollingParent.getClientArea();
+ scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+
+ return scollingParent;
+ }
+
+ /**
+ * Create Radio (on/off/roaming, for voice/data) controls.
+ * @param top
+ */
+ private void createRadioControls(final Composite top) {
+ Group g1 = new Group(top, SWT.NONE);
+ g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ g1.setLayout(new GridLayout(2, false));
+ g1.setText("Telephony Status");
+
+ // the inside of the group is 2 composite so that all the column of the controls (mainly
+ // combos) have the same width, while not taking the whole screen width
+ Composite insideGroup = new Composite(g1, SWT.NONE);
+ GridLayout gl = new GridLayout(4, false);
+ gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+ insideGroup.setLayout(gl);
+
+ mVoiceLabel = new Label(insideGroup, SWT.NONE);
+ mVoiceLabel.setText("Voice:");
+ mVoiceLabel.setAlignment(SWT.RIGHT);
+
+ mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY);
+ mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String[] mode : GSM_MODES) {
+ mVoiceMode.add(mode[0]);
+ }
+ mVoiceMode.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setVoiceMode(mVoiceMode.getSelectionIndex());
+ }
+ });
+
+ mSpeedLabel = new Label(insideGroup, SWT.NONE);
+ mSpeedLabel.setText("Speed:");
+ mSpeedLabel.setAlignment(SWT.RIGHT);
+
+ mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY);
+ mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String mode : NETWORK_SPEEDS) {
+ mNetworkSpeed.add(mode);
+ }
+ mNetworkSpeed.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setNetworkSpeed(mNetworkSpeed.getSelectionIndex());
+ }
+ });
+
+ mDataLabel = new Label(insideGroup, SWT.NONE);
+ mDataLabel.setText("Data:");
+ mDataLabel.setAlignment(SWT.RIGHT);
+
+ mDataMode = new Combo(insideGroup, SWT.READ_ONLY);
+ mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String[] mode : GSM_MODES) {
+ mDataMode.add(mode[0]);
+ }
+ mDataMode.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setDataMode(mDataMode.getSelectionIndex());
+ }
+ });
+
+ mLatencyLabel = new Label(insideGroup, SWT.NONE);
+ mLatencyLabel.setText("Latency:");
+ mLatencyLabel.setAlignment(SWT.RIGHT);
+
+ mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY);
+ mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (String mode : NETWORK_LATENCIES) {
+ mNetworkLatency.add(mode);
+ }
+ mNetworkLatency.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setNetworkLatency(mNetworkLatency.getSelectionIndex());
+ }
+ });
+
+ // now an empty label to take the rest of the width of the group
+ Label l = new Label(g1, SWT.NONE);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ }
+
+ /**
+ * Create Voice/SMS call/hang up controls
+ * @param top
+ */
+ private void createCallControls(final Composite top) {
+ GridLayout gl;
+ Group g2 = new Group(top, SWT.NONE);
+ g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ g2.setLayout(new GridLayout(1, false));
+ g2.setText("Telephony Actions");
+
+ // horizontal composite for label + text field
+ Composite phoneComp = new Composite(g2, SWT.NONE);
+ phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH));
+ gl = new GridLayout(2, false);
+ gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+ phoneComp.setLayout(gl);
+
+ mNumberLabel = new Label(phoneComp, SWT.NONE);
+ mNumberLabel.setText("Incoming number:");
+
+ mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+ mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mPhoneNumber.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ // Reenable the widgets based on the content of the text.
+ // doEnable checks the validity of the phone number to enable/disable some
+ // widgets.
+ // Looks like we're getting a callback at creation time, so we can't
+ // suppose that we are enabled when the text is modified...
+ doEnable(mEmulatorConsole != null);
+ }
+ });
+
+ mVoiceButton = new Button(phoneComp, SWT.RADIO);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 2;
+ mVoiceButton.setText("Voice");
+ mVoiceButton.setLayoutData(gd);
+ mVoiceButton.setEnabled(false);
+ mVoiceButton.setSelection(true);
+ mVoiceButton.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ doEnable(true);
+
+ if (mVoiceButton.getSelection()) {
+ mCallButton.setText("Call");
+ } else {
+ mCallButton.setText("Send");
+ }
+ }
+ });
+
+ mSmsButton = new Button(phoneComp, SWT.RADIO);
+ mSmsButton.setText("SMS");
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ mSmsButton.setLayoutData(gd);
+ mSmsButton.setEnabled(false);
+ // Since there are only 2 radio buttons, we can put a listener on only one (they
+ // are both called on select and unselect event.
+
+ mMessageLabel = new Label(phoneComp, SWT.NONE);
+ gd = new GridData();
+ gd.verticalAlignment = SWT.TOP;
+ mMessageLabel.setLayoutData(gd);
+ mMessageLabel.setText("Message:");
+ mMessageLabel.setEnabled(false);
+
+ mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
+ mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 70;
+ mSmsMessage.setEnabled(false);
+
+ // composite to put the 2 buttons horizontally
+ Composite g2ButtonComp = new Composite(g2, SWT.NONE);
+ g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ gl = new GridLayout(2, false);
+ gl.marginWidth = gl.marginHeight = 0;
+ g2ButtonComp.setLayout(gl);
+
+ // now a button below the phone number
+ mCallButton = new Button(g2ButtonComp, SWT.PUSH);
+ mCallButton.setText("Call");
+ mCallButton.setEnabled(false);
+ mCallButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ if (mVoiceButton.getSelection()) {
+ processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim()));
+ } else {
+ // we need to encode the message. We need to replace the carriage return
+ // character by the 2 character string \n.
+ // Because of this the \ character needs to be escaped as well.
+ // ReplaceAll() expects regexp so \ char are escaped twice.
+ String message = mSmsMessage.getText();
+ message = message.replaceAll("\\\\", //$NON-NLS-1$
+ "\\\\\\\\"); //$NON-NLS-1$
+
+ // While the normal line delimiter is returned by Text.getLineDelimiter()
+ // it seems copy pasting text coming from somewhere else could have another
+ // delimited. For this reason, we'll replace is several steps
+
+ // replace the dual CR-LF
+ message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$
+
+ // replace remaining stand alone \n
+ message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$
+
+ // replace remaining stand alone \r
+ message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$
+
+ processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(),
+ message));
+ }
+ }
+ }
+ });
+
+ mCancelButton = new Button(g2ButtonComp, SWT.PUSH);
+ mCancelButton.setText("Hang Up");
+ mCancelButton.setEnabled(false);
+ mCancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ if (mVoiceButton.getSelection()) {
+ processCommandResult(mEmulatorConsole.cancelCall(
+ mPhoneNumber.getText().trim()));
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Create Location controls.
+ * @param top
+ */
+ private void createLocationControls(final Composite top) {
+ Label l = new Label(top, SWT.NONE);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ l.setText("Location Controls");
+
+ mLocationFolders = new TabFolder(top, SWT.NONE);
+ mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ TabItem item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("Manual");
+ item.setControl(manualLocationComp);
+
+ createManualLocationControl(manualLocationComp);
+
+ mPlayImage = mImageLoader.loadImage("play.png", mParent.getDisplay()); // $NON-NLS-1$
+ mPauseImage = mImageLoader.loadImage("pause.png", mParent.getDisplay()); // $NON-NLS-1$
+
+ Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("GPX");
+ item.setControl(gpxLocationComp);
+
+ createGpxLocationControl(gpxLocationComp);
+
+ Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE);
+ kmlLocationComp.setLayout(new FillLayout());
+ item = new TabItem(mLocationFolders, SWT.NONE);
+ item.setText("KML");
+ item.setControl(kmlLocationComp);
+
+ createKmlLocationControl(kmlLocationComp);
+ }
+
+ private void createManualLocationControl(Composite manualLocationComp) {
+ final StackLayout sl;
+ GridLayout gl;
+ Label label;
+
+ manualLocationComp.setLayout(new GridLayout(1, false));
+ mDecimalButton = new Button(manualLocationComp, SWT.RADIO);
+ mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDecimalButton.setText("Decimal");
+ mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO);
+ mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mSexagesimalButton.setText("Sexagesimal");
+
+ // composite to hold and switching between the 2 modes.
+ final Composite content = new Composite(manualLocationComp, SWT.NONE);
+ content.setLayout(sl = new StackLayout());
+
+ // decimal display
+ final Composite decimalContent = new Composite(content, SWT.NONE);
+ decimalContent.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mLongitudeControls = new CoordinateControls();
+ mLatitudeControls = new CoordinateControls();
+
+ label = new Label(decimalContent, SWT.NONE);
+ label.setText("Longitude");
+
+ mLongitudeControls.createDecimalText(decimalContent);
+
+ label = new Label(decimalContent, SWT.NONE);
+ label.setText("Latitude");
+
+ mLatitudeControls.createDecimalText(decimalContent);
+
+ // sexagesimal content
+ final Composite sexagesimalContent = new Composite(content, SWT.NONE);
+ sexagesimalContent.setLayout(gl = new GridLayout(7, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("Longitude");
+
+ mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\u00B0"); // degree character
+
+ mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("'");
+
+ mLongitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\"");
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("Latitude");
+
+ mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\u00B0");
+
+ mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("'");
+
+ mLatitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+ label = new Label(sexagesimalContent, SWT.NONE);
+ label.setText("\"");
+
+ // set the default display to decimal
+ sl.topControl = decimalContent;
+ mDecimalButton.setSelection(true);
+
+ mDecimalButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mDecimalButton.getSelection()) {
+ sl.topControl = decimalContent;
+ } else {
+ sl.topControl = sexagesimalContent;
+ }
+ content.layout();
+ }
+ });
+
+ Button sendButton = new Button(manualLocationComp, SWT.PUSH);
+ sendButton.setText("Send");
+ sendButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0));
+ }
+ }
+ });
+
+ mLongitudeControls.setValue(DEFAULT_LONGITUDE);
+ mLatitudeControls.setValue(DEFAULT_LATITUDE);
+ }
+
+ private void createGpxLocationControl(Composite gpxLocationComp) {
+ GridData gd;
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ gpxLocationComp.setLayout(new GridLayout(1, false));
+
+ mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH);
+ mGpxUploadButton.setText("Load GPX...");
+
+ // Table for way point
+ mGpxWayPointTable = new Table(gpxLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 100;
+ mGpxWayPointTable.setHeaderVisible(true);
+ mGpxWayPointTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT,
+ "Some Name",
+ PREFS_WAYPOINT_COL_NAME, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LONGITUDE, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LATITUDE, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT,
+ "99999.9",
+ PREFS_WAYPOINT_COL_ELEVATION, store);
+ TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT,
+ "Some Description",
+ PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+ final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable);
+ gpxWayPointViewer.setContentProvider(new WayPointContentProvider());
+ gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+ gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ wayPoint.getLongitude(), wayPoint.getLatitude(),
+ wayPoint.getElevation()));
+ }
+ }
+ }
+ }
+ });
+
+ // table for tracks.
+ mGpxTrackTable = new Table(gpxLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 100;
+ mGpxTrackTable.setHeaderVisible(true);
+ mGpxTrackTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT,
+ "Some very long name",
+ PREFS_TRACK_COL_NAME, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT,
+ "9999",
+ PREFS_TRACK_COL_COUNT, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT,
+ "999-99-99T99:99:99Z",
+ PREFS_TRACK_COL_FIRST, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT,
+ "999-99-99T99:99:99Z",
+ PREFS_TRACK_COL_LAST, store);
+ TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT,
+ "-199.999999",
+ PREFS_TRACK_COL_COMMENT, store);
+
+ final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable);
+ gpxTrackViewer.setContentProvider(new TrackContentProvider());
+ gpxTrackViewer.setLabelProvider(new TrackLabelProvider());
+
+ gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof Track) {
+ Track track = (Track)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ TrackPoint[] points = track.getPoints();
+ processCommandResult(mEmulatorConsole.sendLocation(
+ points[0].getLongitude(), points[0].getLatitude(),
+ points[0].getElevation()));
+ }
+
+ mPlayGpxButton.setEnabled(true);
+ mGpxBackwardButton.setEnabled(true);
+ mGpxForwardButton.setEnabled(true);
+ mGpxSpeedButton.setEnabled(true);
+
+ return;
+ }
+ }
+
+ mPlayGpxButton.setEnabled(false);
+ mGpxBackwardButton.setEnabled(false);
+ mGpxForwardButton.setEnabled(false);
+ mGpxSpeedButton.setEnabled(false);
+ }
+ });
+
+ mGpxUploadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load GPX File");
+ fileDialog.setFilterExtensions(new String[] { "*.gpx" } );
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ GpxParser parser = new GpxParser(fileName);
+ if (parser.parse()) {
+ gpxWayPointViewer.setInput(parser.getWayPoints());
+ gpxTrackViewer.setInput(parser.getTracks());
+ }
+ }
+ }
+ });
+
+ mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE);
+ GridLayout gl;
+ mGpxPlayControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayGpxButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPlayingTrack == false) {
+ ISelection selection = gpxTrackViewer.getSelection();
+ if (selection.isEmpty() == false && selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof Track) {
+ Track track = (Track)selectedObject;
+ playTrack(track);
+ }
+ }
+ } else {
+ // if we're playing, then we pause
+ mPlayingTrack = false;
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ }
+ });
+
+ Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mGpxBackwardButton.setSelection(false);
+ mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mGpxForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mGpxForwardButton.setSelection(true);
+ mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+ mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+
+ mSpeedIndex = 0;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+ mPlayGpxButton.setEnabled(false);
+ mGpxBackwardButton.setEnabled(false);
+ mGpxForwardButton.setEnabled(false);
+ mGpxSpeedButton.setEnabled(false);
+
+ }
+
+ private void createKmlLocationControl(Composite kmlLocationComp) {
+ GridData gd;
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ kmlLocationComp.setLayout(new GridLayout(1, false));
+
+ mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH);
+ mKmlUploadButton.setText("Load KML...");
+
+ // Table for way point
+ mKmlWayPointTable = new Table(kmlLocationComp,
+ SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+ mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.heightHint = 200;
+ mKmlWayPointTable.setHeaderVisible(true);
+ mKmlWayPointTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT,
+ "Some Name",
+ PREFS_WAYPOINT_COL_NAME, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LONGITUDE, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT,
+ "-199.999999",
+ PREFS_WAYPOINT_COL_LATITUDE, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT,
+ "99999.9",
+ PREFS_WAYPOINT_COL_ELEVATION, store);
+ TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT,
+ "Some Description",
+ PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+ final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable);
+ kmlWayPointViewer.setContentProvider(new WayPointContentProvider());
+ kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+ mKmlUploadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load KML File");
+ fileDialog.setFilterExtensions(new String[] { "*.kml" } );
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ KmlParser parser = new KmlParser(fileName);
+ if (parser.parse()) {
+ kmlWayPointViewer.setInput(parser.getWayPoints());
+
+ mPlayKmlButton.setEnabled(true);
+ mKmlBackwardButton.setEnabled(true);
+ mKmlForwardButton.setEnabled(true);
+ mKmlSpeedButton.setEnabled(true);
+ }
+ }
+ }
+ });
+
+ kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object selectedObject = structuredSelection.getFirstElement();
+ if (selectedObject instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)selectedObject;
+
+ if (mEmulatorConsole != null && mPlayingTrack == false) {
+ processCommandResult(mEmulatorConsole.sendLocation(
+ wayPoint.getLongitude(), wayPoint.getLatitude(),
+ wayPoint.getElevation()));
+ }
+ }
+ }
+ }
+ });
+
+
+
+ mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE);
+ GridLayout gl;
+ mKmlPlayControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+ mPlayKmlButton.setImage(mPlayImage);
+ mPlayKmlButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPlayingTrack == false) {
+ Object input = kmlWayPointViewer.getInput();
+ if (input instanceof WayPoint[]) {
+ playKml((WayPoint[])input);
+ }
+ } else {
+ // if we're playing, then we pause
+ mPlayingTrack = false;
+ if (mPlayingThread != null) {
+ mPlayingThread.interrupt();
+ }
+ }
+ }
+ });
+
+ Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mKmlBackwardButton.setSelection(false);
+ mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+ mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+ mKmlForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$
+ mKmlForwardButton.setSelection(true);
+ mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+ mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+
+ mSpeedIndex = 0;
+ mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+ mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+ mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+ mPlayKmlButton.setEnabled(false);
+ mKmlBackwardButton.setEnabled(false);
+ mKmlForwardButton.setEnabled(false);
+ mKmlSpeedButton.setEnabled(false);
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ }
+
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ private synchronized void setDataMode(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setGsmDataMode(
+ GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+ }
+ }
+
+ private synchronized void setVoiceMode(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setGsmVoiceMode(
+ GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+ }
+ }
+
+ private synchronized void setNetworkLatency(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex));
+ }
+ }
+
+ private synchronized void setNetworkSpeed(int selectionIndex) {
+ if (mEmulatorConsole != null) {
+ processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex));
+ }
+ }
+
+
+ /**
+ * Callback on device selection change.
+ * @param device the new selected device
+ */
+ public void handleNewDevice(Device device) {
+ if (mParent.isDisposed()) {
+ return;
+ }
+ // unlink to previous console.
+ synchronized (this) {
+ mEmulatorConsole = null;
+ }
+
+ try {
+ // get the emulator console for this device
+ // First we need the device itself
+ if (device != null) {
+ GsmStatus gsm = null;
+ NetworkStatus netstatus = null;
+
+ synchronized (this) {
+ mEmulatorConsole = EmulatorConsole.getConsole(device);
+ if (mEmulatorConsole != null) {
+ // get the gsm status
+ gsm = mEmulatorConsole.getGsmStatus();
+ netstatus = mEmulatorConsole.getNetworkStatus();
+
+ if (gsm == null || netstatus == null) {
+ mEmulatorConsole = null;
+ }
+ }
+ }
+
+ if (gsm != null && netstatus != null) {
+ Display d = mParent.getDisplay();
+ if (d.isDisposed() == false) {
+ final GsmStatus f_gsm = gsm;
+ final NetworkStatus f_netstatus = netstatus;
+
+ d.asyncExec(new Runnable() {
+ public void run() {
+ if (f_gsm.voice != GsmMode.UNKNOWN) {
+ mVoiceMode.select(getGsmComboIndex(f_gsm.voice));
+ } else {
+ mVoiceMode.clearSelection();
+ }
+ if (f_gsm.data != GsmMode.UNKNOWN) {
+ mDataMode.select(getGsmComboIndex(f_gsm.data));
+ } else {
+ mDataMode.clearSelection();
+ }
+
+ if (f_netstatus.speed != -1) {
+ mNetworkSpeed.select(f_netstatus.speed);
+ } else {
+ mNetworkSpeed.clearSelection();
+ }
+
+ if (f_netstatus.latency != -1) {
+ mNetworkLatency.select(f_netstatus.latency);
+ } else {
+ mNetworkLatency.clearSelection();
+ }
+ }
+ });
+ }
+ }
+ }
+ } finally {
+ // enable/disable the ui
+ boolean enable = false;
+ synchronized (this) {
+ enable = mEmulatorConsole != null;
+ }
+
+ enable(enable);
+ }
+ }
+
+ /**
+ * Enable or disable the ui. Can be called from non ui threads.
+ * @param enabled
+ */
+ private void enable(final boolean enabled) {
+ try {
+ Display d = mParent.getDisplay();
+ d.asyncExec(new Runnable() {
+ public void run() {
+ if (mParent.isDisposed() == false) {
+ doEnable(enabled);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // disposed. do nothing
+ }
+ }
+
+ private boolean isValidPhoneNumber() {
+ String number = mPhoneNumber.getText().trim();
+
+ return number.matches(RE_PHONE_NUMBER);
+ }
+
+ /**
+ * Enable or disable the ui. Cannot be called from non ui threads.
+ * @param enabled
+ */
+ protected void doEnable(boolean enabled) {
+ mVoiceLabel.setEnabled(enabled);
+ mVoiceMode.setEnabled(enabled);
+
+ mDataLabel.setEnabled(enabled);
+ mDataMode.setEnabled(enabled);
+
+ mSpeedLabel.setEnabled(enabled);
+ mNetworkSpeed.setEnabled(enabled);
+
+ mLatencyLabel.setEnabled(enabled);
+ mNetworkLatency.setEnabled(enabled);
+
+ // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+ // if we don't need to.
+ if (mPhoneNumber.isEnabled() != enabled) {
+ mNumberLabel.setEnabled(enabled);
+ mPhoneNumber.setEnabled(enabled);
+ }
+
+ boolean valid = isValidPhoneNumber();
+
+ mVoiceButton.setEnabled(enabled && valid);
+ mSmsButton.setEnabled(enabled && valid);
+
+ boolean smsValid = enabled && valid && mSmsButton.getSelection();
+
+ // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+ // if we don't need to.
+ if (mSmsMessage.isEnabled() != smsValid) {
+ mMessageLabel.setEnabled(smsValid);
+ mSmsMessage.setEnabled(smsValid);
+ }
+ if (enabled == false) {
+ mSmsMessage.setText(""); //$NON-NLs-1$
+ }
+
+ mCallButton.setEnabled(enabled && valid);
+ mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection());
+
+ if (enabled == false) {
+ mVoiceMode.clearSelection();
+ mDataMode.clearSelection();
+ mNetworkSpeed.clearSelection();
+ mNetworkLatency.clearSelection();
+ if (mPhoneNumber.getText().length() > 0) {
+ mPhoneNumber.setText(""); //$NON-NLS-1$
+ }
+ }
+
+ // location controls
+ mLocationFolders.setEnabled(enabled);
+
+ mDecimalButton.setEnabled(enabled);
+ mSexagesimalButton.setEnabled(enabled);
+ mLongitudeControls.setEnabled(enabled);
+ mLatitudeControls.setEnabled(enabled);
+
+ mGpxUploadButton.setEnabled(enabled);
+ mGpxWayPointTable.setEnabled(enabled);
+ mGpxTrackTable.setEnabled(enabled);
+ mKmlUploadButton.setEnabled(enabled);
+ mKmlWayPointTable.setEnabled(enabled);
+ }
+
+ /**
+ * Returns the index of the combo item matching a specific GsmMode.
+ * @param mode
+ */
+ private int getGsmComboIndex(GsmMode mode) {
+ for (int i = 0 ; i < GSM_MODES.length; i++) {
+ String[] modes = GSM_MODES[i];
+ if (mode.getTag().equals(modes[1])) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Processes the result of a command sent to the console.
+ * @param result the result of the command.
+ */
+ private boolean processCommandResult(final String result) {
+ if (result != EmulatorConsole.RESULT_OK) {
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ if (mParent.isDisposed() == false) {
+ MessageDialog.openError(mParent.getShell(), "Emulator Console",
+ result);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param track
+ */
+ private void playTrack(final Track track) {
+ // no need to synchronize this check, the worst that can happen, is we start the thread
+ // for nothing.
+ if (mEmulatorConsole != null) {
+ mPlayGpxButton.setImage(mPauseImage);
+ mPlayKmlButton.setImage(mPauseImage);
+ mPlayingTrack = true;
+
+ mPlayingThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ TrackPoint[] trackPoints = track.getPoints();
+ int count = trackPoints.length;
+
+ // get the start index.
+ int start = 0;
+ if (mPlayDirection == -1) {
+ start = count - 1;
+ }
+
+ for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // get the current point and send its location to
+ // the emulator.
+ final TrackPoint trackPoint = trackPoints[p];
+
+ synchronized (EmulatorControlPanel.this) {
+ if (mEmulatorConsole == null ||
+ processCommandResult(mEmulatorConsole.sendLocation(
+ trackPoint.getLongitude(), trackPoint.getLatitude(),
+ trackPoint.getElevation())) == false) {
+ return;
+ }
+ }
+
+ // if this is not the final point, then get the next one and
+ // compute the delta time
+ int nextIndex = p + mPlayDirection;
+ if (nextIndex >=0 && nextIndex < count) {
+ TrackPoint nextPoint = trackPoints[nextIndex];
+
+ long delta = nextPoint.getTime() - trackPoint.getTime();
+ if (delta < 0) {
+ delta = -delta;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ try {
+ sleep(delta / mSpeed);
+ } catch (InterruptedException e) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // we got interrupted, lets make sure we can play
+ do {
+ long waited = System.currentTimeMillis() - startTime;
+ long needToWait = delta / mSpeed;
+ if (waited < needToWait) {
+ try {
+ sleep(needToWait - waited);
+ } catch (InterruptedException e1) {
+ // we'll just loop and wait again if needed.
+ // unless we're supposed to stop
+ if (mPlayingTrack == false) {
+ return;
+ }
+ }
+ } else {
+ break;
+ }
+ } while (true);
+ }
+ }
+ }
+ } finally {
+ mPlayingTrack = false;
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ if (mPlayGpxButton.isDisposed() == false) {
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayKmlButton.setImage(mPlayImage);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+ }
+ }
+ };
+
+ mPlayingThread.start();
+ }
+ }
+
+ private void playKml(final WayPoint[] trackPoints) {
+ // no need to synchronize this check, the worst that can happen, is we start the thread
+ // for nothing.
+ if (mEmulatorConsole != null) {
+ mPlayGpxButton.setImage(mPauseImage);
+ mPlayKmlButton.setImage(mPauseImage);
+ mPlayingTrack = true;
+
+ mPlayingThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ int count = trackPoints.length;
+
+ // get the start index.
+ int start = 0;
+ if (mPlayDirection == -1) {
+ start = count - 1;
+ }
+
+ for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // get the current point and send its location to
+ // the emulator.
+ WayPoint trackPoint = trackPoints[p];
+
+ synchronized (EmulatorControlPanel.this) {
+ if (mEmulatorConsole == null ||
+ processCommandResult(mEmulatorConsole.sendLocation(
+ trackPoint.getLongitude(), trackPoint.getLatitude(),
+ trackPoint.getElevation())) == false) {
+ return;
+ }
+ }
+
+ // if this is not the final point, then get the next one and
+ // compute the delta time
+ int nextIndex = p + mPlayDirection;
+ if (nextIndex >=0 && nextIndex < count) {
+
+ long delta = 1000; // 1 second
+ if (delta < 0) {
+ delta = -delta;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ try {
+ sleep(delta / mSpeed);
+ } catch (InterruptedException e) {
+ if (mPlayingTrack == false) {
+ return;
+ }
+
+ // we got interrupted, lets make sure we can play
+ do {
+ long waited = System.currentTimeMillis() - startTime;
+ long needToWait = delta / mSpeed;
+ if (waited < needToWait) {
+ try {
+ sleep(needToWait - waited);
+ } catch (InterruptedException e1) {
+ // we'll just loop and wait again if needed.
+ // unless we're supposed to stop
+ if (mPlayingTrack == false) {
+ return;
+ }
+ }
+ } else {
+ break;
+ }
+ } while (true);
+ }
+ }
+ }
+ } finally {
+ mPlayingTrack = false;
+ try {
+ mParent.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ if (mPlayGpxButton.isDisposed() == false) {
+ mPlayGpxButton.setImage(mPlayImage);
+ mPlayKmlButton.setImage(mPlayImage);
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // we're quitting, just ignore
+ }
+ }
+ }
+ };
+
+ mPlayingThread.start();
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java
new file mode 100644
index 0000000..977203b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java
@@ -0,0 +1,1294 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.CategoryLabelPositions;
+import org.jfree.chart.labels.CategoryToolTipGenerator;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.Plot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.CategoryItemRenderer;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Base class for our information panels.
+ */
+public final class HeapPanel extends BaseHeapPanel {
+ private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$
+ private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$
+
+ /* args to setUpdateStatus() */
+ private static final int NOT_SELECTED = 0;
+ private static final int NOT_ENABLED = 1;
+ private static final int ENABLED = 2;
+
+ /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+ * Native+1 at least. We also need 2 more entries for free area and expansion area. */
+ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+ private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+ private static final PaletteData mMapPalette = createPalette();
+
+ private static final boolean DISPLAY_HEAP_BITMAP = false;
+ private static final boolean DISPLAY_HILBERT_BITMAP = false;
+
+ private static final int PLACEHOLDER_HILBERT_SIZE = 200;
+ private static final int PLACEHOLDER_LINEAR_V_SIZE = 100;
+ private static final int PLACEHOLDER_LINEAR_H_SIZE = 300;
+
+ private static final int[] ZOOMS = {100, 50, 25};
+
+ private static final NumberFormat sByteFormatter = NumberFormat.getInstance();
+ private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance();
+ private static final NumberFormat sCountFormatter = NumberFormat.getInstance();
+
+ static {
+ sByteFormatter.setMinimumFractionDigits(0);
+ sByteFormatter.setMaximumFractionDigits(1);
+ sLargeByteFormatter.setMinimumFractionDigits(3);
+ sLargeByteFormatter.setMaximumFractionDigits(3);
+
+ sCountFormatter.setGroupingUsed(true);
+ }
+
+ private Display mDisplay;
+
+ private Composite mTop; // real top
+ private Label mUpdateStatus;
+ private Table mHeapSummary;
+ private Combo mDisplayMode;
+
+ //private ScrolledComposite mScrolledComposite;
+
+ private Composite mDisplayBase; // base of the displays.
+ private StackLayout mDisplayStack;
+
+ private Composite mStatisticsBase;
+ private Table mStatisticsTable;
+ private JFreeChart mChart;
+ private ChartComposite mChartComposite;
+ private Button mGcButton;
+ private DefaultCategoryDataset mAllocCountDataSet;
+
+ private Composite mLinearBase;
+ private Label mLinearHeapImage;
+
+ private Composite mHilbertBase;
+ private Label mHilbertHeapImage;
+ private Group mLegend;
+ private Combo mZoom;
+
+ /** Image used for the hilbert display. Since we recreate a new image every time, we
+ * keep this one around to dispose it. */
+ private Image mHilbertImage;
+ private Image mLinearImage;
+ private Composite[] mLayout;
+
+ /*
+ * Create color palette for map. Set up titles for legend.
+ */
+ private static PaletteData createPalette() {
+ RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+ colors[0]
+ = new RGB(192, 192, 192); // non-heap pixels are gray
+ mMapLegend[0]
+ = "(heap expansion area)";
+
+ colors[1]
+ = new RGB(0, 0, 0); // free chunks are black
+ mMapLegend[1]
+ = "free";
+
+ colors[HeapSegmentElement.KIND_OBJECT + 2]
+ = new RGB(0, 0, 255); // objects are blue
+ mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+ = "data object";
+
+ colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = new RGB(0, 255, 0); // class objects are green
+ mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = "class object";
+
+ colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = new RGB(255, 0, 0); // byte/bool arrays are red
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = "1-byte array (byte[], boolean[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = new RGB(255, 128, 0); // short/char arrays are orange
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = "2-byte array (short[], char[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = new RGB(255, 255, 0); // obj/int/float arrays are yellow
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = "4-byte array (object[], int[], float[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = new RGB(255, 128, 128); // long/double arrays are pink
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = "8-byte array (long[], double[])";
+
+ colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = new RGB(255, 0, 255); // unknown objects are cyan
+ mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = "unknown object";
+
+ colors[HeapSegmentElement.KIND_NATIVE + 2]
+ = new RGB(64, 64, 64); // native objects are dark gray
+ mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+ = "non-Java object";
+
+ return new PaletteData(colors);
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE ||
+ (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) {
+ try {
+ mTop.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ clientSelected();
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed (app is quitting most likely), we do nothing.
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mTop.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ Log.d("ddms", "HeapPanel: changed " + client);
+
+ if (client != null) {
+ ClientData cd = client.getClientData();
+
+ if (client.isHeapUpdateEnabled()) {
+ mGcButton.setEnabled(true);
+ mDisplayMode.setEnabled(true);
+ setUpdateStatus(ENABLED);
+ } else {
+ setUpdateStatus(NOT_ENABLED);
+ mGcButton.setEnabled(false);
+ mDisplayMode.setEnabled(false);
+ }
+
+ fillSummaryTable(cd);
+
+ int mode = mDisplayMode.getSelectionIndex();
+ if (mode == 0) {
+ fillDetailedTable(client, false /* forceRedraw */);
+ } else {
+ if (DISPLAY_HEAP_BITMAP) {
+ renderHeapData(cd, mode - 1, false /* forceRedraw */);
+ }
+ }
+ } else {
+ mGcButton.setEnabled(false);
+ mDisplayMode.setEnabled(false);
+ fillSummaryTable(null);
+ fillDetailedTable(null, true);
+ setUpdateStatus(NOT_SELECTED);
+ }
+
+ // sizes of things change frequently, so redo layout
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ GridLayout gl;
+
+ mTop = new Composite(parent, SWT.NONE);
+ mTop.setLayout(new GridLayout(1, false));
+ mTop.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mUpdateStatus = new Label(mTop, SWT.NONE);
+ setUpdateStatus(NOT_SELECTED);
+
+ Composite summarySection = new Composite(mTop, SWT.NONE);
+ summarySection.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mHeapSummary = createSummaryTable(summarySection);
+ mGcButton = new Button(summarySection, SWT.PUSH);
+ mGcButton.setText("Cause GC");
+ mGcButton.setEnabled(false);
+ mGcButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.executeGarbageCollector();
+ }
+ }
+ });
+
+ Composite comboSection = new Composite(mTop, SWT.NONE);
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ comboSection.setLayout(gl);
+
+ Label displayLabel = new Label(comboSection, SWT.NONE);
+ displayLabel.setText("Display: ");
+
+ mDisplayMode = new Combo(comboSection, SWT.READ_ONLY);
+ mDisplayMode.setEnabled(false);
+ mDisplayMode.add("Stats");
+ if (DISPLAY_HEAP_BITMAP) {
+ mDisplayMode.add("Linear");
+ if (DISPLAY_HILBERT_BITMAP) {
+ mDisplayMode.add("Hilbert");
+ }
+ }
+
+ // the base of the displays.
+ mDisplayBase = new Composite(mTop, SWT.NONE);
+ mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mDisplayStack = new StackLayout();
+ mDisplayBase.setLayout(mDisplayStack);
+
+ // create the statistics display
+ mStatisticsBase = new Composite(mDisplayBase, SWT.NONE);
+ //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mStatisticsBase.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mDisplayStack.topControl = mStatisticsBase;
+
+ mStatisticsTable = createDetailedTable(mStatisticsBase);
+ mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createChart();
+
+ //create the linear composite
+ mLinearBase = new Composite(mDisplayBase, SWT.NONE);
+ //mLinearBase.setLayoutData(new GridData());
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ mLinearBase.setLayout(gl);
+
+ {
+ mLinearHeapImage = new Label(mLinearBase, SWT.NONE);
+ mLinearHeapImage.setLayoutData(new GridData());
+ mLinearHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay,
+ PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE,
+ mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+ // create a composite to contain the bottom part (legend)
+ Composite bottomSection = new Composite(mLinearBase, SWT.NONE);
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ bottomSection.setLayout(gl);
+
+ createLegend(bottomSection);
+ }
+
+/*
+ mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL);
+ mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mScrolledComposite.setExpandHorizontal(true);
+ mScrolledComposite.setExpandVertical(true);
+ mScrolledComposite.setContent(mDisplayBase);
+*/
+
+
+ // create the hilbert display.
+ mHilbertBase = new Composite(mDisplayBase, SWT.NONE);
+ //mHilbertBase.setLayoutData(new GridData());
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ mHilbertBase.setLayout(gl);
+
+ if (DISPLAY_HILBERT_BITMAP) {
+ mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE);
+ mHilbertHeapImage.setLayoutData(new GridData());
+ mHilbertHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay,
+ PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE,
+ mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+ // create a composite to contain the right part (legend + zoom)
+ Composite rightSection = new Composite(mHilbertBase, SWT.NONE);
+ gl = new GridLayout(1, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ rightSection.setLayout(gl);
+
+ Composite zoomComposite = new Composite(rightSection, SWT.NONE);
+ gl = new GridLayout(2, false);
+ zoomComposite.setLayout(gl);
+
+ Label l = new Label(zoomComposite, SWT.NONE);
+ l.setText("Zoom:");
+ mZoom = new Combo(zoomComposite, SWT.READ_ONLY);
+ for (int z : ZOOMS) {
+ mZoom.add(String.format("%1$d%%", z)); // $NON-NLS-1$
+ }
+
+ mZoom.select(0);
+ mZoom.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setLegendText(mZoom.getSelectionIndex());
+ Client client = getCurrentClient();
+ if (client != null) {
+ renderHeapData(client.getClientData(), 1, true);
+ mTop.pack();
+ }
+ }
+ });
+
+ createLegend(rightSection);
+ }
+ mHilbertBase.pack();
+
+ mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase };
+ mDisplayMode.select(0);
+ mDisplayMode.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = mDisplayMode.getSelectionIndex();
+ Client client = getCurrentClient();
+
+ if (client != null) {
+ if (index == 0) {
+ fillDetailedTable(client, true /* forceRedraw */);
+ } else {
+ renderHeapData(client.getClientData(), index-1, true /* forceRedraw */);
+ }
+ }
+
+ mDisplayStack.topControl = mLayout[index];
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+ }
+ });
+
+ //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+ // SWT.DEFAULT));
+ mDisplayBase.layout();
+ //mScrolledComposite.redraw();
+
+ return mTop;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mHeapSummary.setFocus();
+ }
+
+
+ private Table createSummaryTable(Composite base) {
+ Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+ tab.setHeaderVisible(true);
+ tab.setLinesVisible(true);
+
+ TableColumn col;
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("ID");
+ col.pack();
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); // $NON-NLS-1$
+ col.pack();
+ col.setText("Heap Size");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); // $NON-NLS-1$
+ col.pack();
+ col.setText("Allocated");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.000WW"); // $NON-NLS-1$
+ col.pack();
+ col.setText("Free");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000.00%"); // $NON-NLS-1$
+ col.pack();
+ col.setText("% Used");
+
+ col = new TableColumn(tab, SWT.RIGHT);
+ col.setText("000,000,000"); // $NON-NLS-1$
+ col.pack();
+ col.setText("# Objects");
+
+ return tab;
+ }
+
+ private Table createDetailedTable(Composite base) {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+ tab.setHeaderVisible(true);
+ tab.setLinesVisible(true);
+
+ TableHelper.createTableColumn(tab, "Type", SWT.LEFT,
+ "4-byte array (object[], int[], float[])", //$NON-NLS-1$
+ PREFS_STATS_COL_TYPE, store);
+
+ TableHelper.createTableColumn(tab, "Count", SWT.RIGHT,
+ "00,000", //$NON-NLS-1$
+ PREFS_STATS_COL_COUNT, store);
+
+ TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_SIZE, store);
+
+ TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_SMALLEST, store);
+
+ TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_LARGEST, store);
+
+ TableHelper.createTableColumn(tab, "Median", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_MEDIAN, store);
+
+ TableHelper.createTableColumn(tab, "Average", SWT.RIGHT,
+ "000.000 WW", //$NON-NLS-1$
+ PREFS_STATS_COL_AVERAGE, store);
+
+ tab.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+
+ Client client = getCurrentClient();
+ if (client != null) {
+ int index = mStatisticsTable.getSelectionIndex();
+ TableItem item = mStatisticsTable.getItem(index);
+
+ if (item != null) {
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+ client.getClientData().getVmHeapData().getProcessedHeapMap();
+
+ ArrayList<HeapSegmentElement> list = heapMap.get(item.getData());
+ if (list != null) {
+ showChart(list);
+ }
+ }
+ }
+
+ }
+ });
+
+ return tab;
+ }
+
+ /**
+ * Creates the chart below the statistics table
+ */
+ private void createChart() {
+ mAllocCountDataSet = new DefaultCategoryDataset();
+ mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet,
+ PlotOrientation.VERTICAL, false, true, false);
+
+ // get the font to make a proper title. We need to convert the swt font,
+ // into an awt font.
+ Font f = mStatisticsBase.getFont();
+ FontData[] fData = f.getFontData();
+
+ // event though on Mac OS there could be more than one fontData, we'll only use
+ // the first one.
+ FontData firstFontData = fData[0];
+
+ java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(),
+ firstFontData, true /* ensureSameSize */);
+
+ mChart.setTitle(new TextTitle("Allocation count per size", awtFont));
+
+ Plot plot = mChart.getPlot();
+ if (plot instanceof CategoryPlot) {
+ // get the plot
+ CategoryPlot categoryPlot = (CategoryPlot)plot;
+
+ // set the domain axis to draw labels that are displayed even with many values.
+ CategoryAxis domainAxis = categoryPlot.getDomainAxis();
+ domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90);
+
+ CategoryItemRenderer renderer = categoryPlot.getRenderer();
+ renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() {
+ public String generateToolTip(CategoryDataset dataset, int row, int column) {
+ // get the key for the size of the allocation
+ ByteLong columnKey = (ByteLong)dataset.getColumnKey(column);
+ String rowKey = (String)dataset.getRowKey(row);
+ Number value = dataset.getValue(rowKey, columnKey);
+
+ return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey,
+ columnKey.getValue());
+ }
+ });
+ }
+ mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000, // max draw width. We don't want it to zoom, so we put a big number
+ 3000, // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ false, // zoom
+ true); // tooltips
+
+ mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ }
+
+ private static String prettyByteCount(long bytes) {
+ double fracBytes = bytes;
+ String units = " B";
+ if (fracBytes < 1024) {
+ return sByteFormatter.format(fracBytes) + units;
+ } else {
+ fracBytes /= 1024;
+ units = " KB";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = " MB";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = " GB";
+ }
+
+ return sLargeByteFormatter.format(fracBytes) + units;
+ }
+
+ private static String approximateByteCount(long bytes) {
+ double fracBytes = bytes;
+ String units = "";
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "K";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "M";
+ }
+ if (fracBytes >= 1024) {
+ fracBytes /= 1024;
+ units = "G";
+ }
+
+ return sByteFormatter.format(fracBytes) + units;
+ }
+
+ private static String addCommasToNumber(long num) {
+ return sCountFormatter.format(num);
+ }
+
+ private static String fractionalPercent(long num, long denom) {
+ double val = (double)num / (double)denom;
+ val *= 100;
+
+ NumberFormat nf = NumberFormat.getInstance();
+ nf.setMinimumFractionDigits(2);
+ nf.setMaximumFractionDigits(2);
+ return nf.format(val) + "%";
+ }
+
+ private void fillSummaryTable(ClientData cd) {
+ if (mHeapSummary.isDisposed()) {
+ return;
+ }
+
+ mHeapSummary.setRedraw(false);
+ mHeapSummary.removeAll();
+
+ if (cd != null) {
+ synchronized (cd) {
+ Iterator<Integer> iter = cd.getVmHeapIds();
+
+ while (iter.hasNext()) {
+ Integer id = iter.next();
+ Map<String, Long> heapInfo = cd.getVmHeapInfo(id);
+ if (heapInfo == null) {
+ continue;
+ }
+ long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES);
+ long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED);
+ long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED);
+
+ TableItem item = new TableItem(mHeapSummary, SWT.NONE);
+ item.setText(0, id.toString());
+
+ item.setText(1, prettyByteCount(sizeInBytes));
+ item.setText(2, prettyByteCount(bytesAllocated));
+ item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated));
+ item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes));
+ item.setText(5, addCommasToNumber(objectsAllocated));
+ }
+ }
+ }
+
+ mHeapSummary.pack();
+ mHeapSummary.setRedraw(true);
+ }
+
+ private void fillDetailedTable(Client client, boolean forceRedraw) {
+ // first check if the client is invalid or heap updates are not enabled.
+ if (client == null || client.isHeapUpdateEnabled() == false) {
+ mStatisticsTable.removeAll();
+ showChart(null);
+ return;
+ }
+
+ ClientData cd = client.getClientData();
+
+ Map<Integer, ArrayList<HeapSegmentElement>> heapMap;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+ // no change, we return.
+ return;
+ }
+
+ heapMap = cd.getVmHeapData().getProcessedHeapMap();
+ }
+
+ // we have new data, lets display it.
+
+ // First, get the current selection, and its key.
+ int index = mStatisticsTable.getSelectionIndex();
+ Integer selectedKey = null;
+ if (index != -1) {
+ selectedKey = (Integer)mStatisticsTable.getItem(index).getData();
+ }
+
+ // disable redraws and remove all from the table.
+ mStatisticsTable.setRedraw(false);
+ mStatisticsTable.removeAll();
+
+ if (heapMap != null) {
+ int selectedIndex = -1;
+ ArrayList<HeapSegmentElement> selectedList = null;
+
+ // get the keys
+ Set<Integer> keys = heapMap.keySet();
+ int iter = 0; // use a manual iter int because Set<?> doesn't have an index
+ // based accessor.
+ for (Integer key : keys) {
+ ArrayList<HeapSegmentElement> list = heapMap.get(key);
+
+ // check if this is the key that is supposed to be selected
+ if (key.equals(selectedKey)) {
+ selectedIndex = iter;
+ selectedList = list;
+ }
+ iter++;
+
+ TableItem item = new TableItem(mStatisticsTable, SWT.NONE);
+ item.setData(key);
+
+ // get the type
+ item.setText(0, mMapLegend[key]);
+
+ // set the count, smallest, largest
+ int count = list.size();
+ item.setText(1, addCommasToNumber(count));
+
+ if (count > 0) {
+ item.setText(3, prettyByteCount(list.get(0).getLength()));
+ item.setText(4, prettyByteCount(list.get(count-1).getLength()));
+
+ int median = count / 2;
+ HeapSegmentElement element = list.get(median);
+ long size = element.getLength();
+ item.setText(5, prettyByteCount(size));
+
+ long totalSize = 0;
+ for (int i = 0 ; i < count; i++) {
+ element = list.get(i);
+
+ size = element.getLength();
+ totalSize += size;
+ }
+
+ // set the average and total
+ item.setText(2, prettyByteCount(totalSize));
+ item.setText(6, prettyByteCount(totalSize / count));
+ }
+ }
+
+ mStatisticsTable.setRedraw(true);
+
+ if (selectedIndex != -1) {
+ mStatisticsTable.setSelection(selectedIndex);
+ showChart(selectedList);
+ } else {
+ showChart(null);
+ }
+ } else {
+ mStatisticsTable.setRedraw(true);
+ }
+ }
+
+ private static class ByteLong implements Comparable<ByteLong> {
+ private long mValue;
+
+ private ByteLong(long value) {
+ mValue = value;
+ }
+
+ public long getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return approximateByteCount(mValue);
+ }
+
+ public int compareTo(ByteLong other) {
+ if (mValue != other.mValue) {
+ return mValue < other.mValue ? -1 : 1;
+ }
+ return 0;
+ }
+
+ }
+
+ /**
+ * Fills the chart with the content of the list of {@link HeapSegmentElement}.
+ */
+ private void showChart(ArrayList<HeapSegmentElement> list) {
+ mAllocCountDataSet.clear();
+
+ if (list != null) {
+ String rowKey = "Alloc Count";
+
+ long currentSize = -1;
+ int currentCount = 0;
+ for (HeapSegmentElement element : list) {
+ if (element.getLength() != currentSize) {
+ if (currentSize != -1) {
+ ByteLong columnKey = new ByteLong(currentSize);
+ mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+ }
+
+ currentSize = element.getLength();
+ currentCount = 1;
+ } else {
+ currentCount++;
+ }
+ }
+
+ // add the last item
+ if (currentSize != -1) {
+ ByteLong columnKey = new ByteLong(currentSize);
+ mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+ }
+ }
+ }
+
+ /*
+ * Add a color legend to the specified table.
+ */
+ private void createLegend(Composite parent) {
+ mLegend = new Group(parent, SWT.NONE);
+ mLegend.setText(getLegendText(0));
+
+ mLegend.setLayout(new GridLayout(2, false));
+
+ RGB[] colors = mMapPalette.colors;
+
+ for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) {
+ Image tmpImage = createColorRect(parent.getDisplay(), colors[i]);
+
+ Label l = new Label(mLegend, SWT.NONE);
+ l.setImage(tmpImage);
+
+ l = new Label(mLegend, SWT.NONE);
+ l.setText(mMapLegend[i]);
+ }
+ }
+
+ private String getLegendText(int level) {
+ int bytes = 8 * (100 / ZOOMS[level]);
+
+ return String.format("Key (1 pixel = %1$d bytes)", bytes);
+ }
+
+ private void setLegendText(int level) {
+ mLegend.setText(getLegendText(level));
+
+ }
+
+ /*
+ * Create a nice rectangle in the specified color.
+ */
+ private Image createColorRect(Display display, RGB color) {
+ int width = 32;
+ int height = 16;
+
+ Image img = new Image(display, width, height);
+ GC gc = new GC(img);
+ gc.setBackground(new Color(display, color));
+ gc.fillRectangle(0, 0, width, height);
+ gc.dispose();
+ return img;
+ }
+
+
+ /*
+ * Are updates enabled?
+ */
+ private void setUpdateStatus(int status) {
+ switch (status) {
+ case NOT_SELECTED:
+ mUpdateStatus.setText("Select a client to see heap updates");
+ break;
+ case NOT_ENABLED:
+ mUpdateStatus.setText("Heap updates are " +
+ "NOT ENABLED for this client");
+ break;
+ case ENABLED:
+ mUpdateStatus.setText("Heap updates will happen after " +
+ "every GC for this client");
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
+ mUpdateStatus.pack();
+ }
+
+
+ /**
+ * Return the closest power of two greater than or equal to value.
+ *
+ * @param value the return value will be >= value
+ * @return a power of two >= value. If value > 2^31, 2^31 is returned.
+ */
+//xxx use Integer.highestOneBit() or numberOfLeadingZeros().
+ private int nextPow2(int value) {
+ for (int i = 31; i >= 0; --i) {
+ if ((value & (1<<i)) != 0) {
+ if (i < 31) {
+ return 1<<(i + 1);
+ } else {
+ return 1<<31;
+ }
+ }
+ }
+ return 0;
+ }
+
+ private int zOrderData(ImageData id, byte pixData[]) {
+ int maxX = 0;
+ for (int i = 0; i < pixData.length; i++) {
+ /* Tread the pixData index as a z-order curve index and
+ * decompose into Cartesian coordinates.
+ */
+ int x = (i & 1) |
+ ((i >>> 2) & 1) << 1 |
+ ((i >>> 4) & 1) << 2 |
+ ((i >>> 6) & 1) << 3 |
+ ((i >>> 8) & 1) << 4 |
+ ((i >>> 10) & 1) << 5 |
+ ((i >>> 12) & 1) << 6 |
+ ((i >>> 14) & 1) << 7 |
+ ((i >>> 16) & 1) << 8 |
+ ((i >>> 18) & 1) << 9 |
+ ((i >>> 20) & 1) << 10 |
+ ((i >>> 22) & 1) << 11 |
+ ((i >>> 24) & 1) << 12 |
+ ((i >>> 26) & 1) << 13 |
+ ((i >>> 28) & 1) << 14 |
+ ((i >>> 30) & 1) << 15;
+ int y = ((i >>> 1) & 1) << 0 |
+ ((i >>> 3) & 1) << 1 |
+ ((i >>> 5) & 1) << 2 |
+ ((i >>> 7) & 1) << 3 |
+ ((i >>> 9) & 1) << 4 |
+ ((i >>> 11) & 1) << 5 |
+ ((i >>> 13) & 1) << 6 |
+ ((i >>> 15) & 1) << 7 |
+ ((i >>> 17) & 1) << 8 |
+ ((i >>> 19) & 1) << 9 |
+ ((i >>> 21) & 1) << 10 |
+ ((i >>> 23) & 1) << 11 |
+ ((i >>> 25) & 1) << 12 |
+ ((i >>> 27) & 1) << 13 |
+ ((i >>> 29) & 1) << 14 |
+ ((i >>> 31) & 1) << 15;
+ try {
+ id.setPixel(x, y, pixData[i]);
+ if (x > maxX) {
+ maxX = x;
+ }
+ } catch (IllegalArgumentException ex) {
+ System.out.println("bad pixels: i " + i +
+ ", w " + id.width +
+ ", h " + id.height +
+ ", x " + x +
+ ", y " + y);
+ throw ex;
+ }
+ }
+ return maxX;
+ }
+
+ private final static int HILBERT_DIR_N = 0;
+ private final static int HILBERT_DIR_S = 1;
+ private final static int HILBERT_DIR_E = 2;
+ private final static int HILBERT_DIR_W = 3;
+
+ private void hilbertWalk(ImageData id, InputStream pixData,
+ int order, int x, int y, int dir)
+ throws IOException {
+ if (x >= id.width || y >= id.height) {
+ return;
+ } else if (order == 0) {
+ try {
+ int p = pixData.read();
+ if (p >= 0) {
+ // flip along x=y axis; assume width == height
+ id.setPixel(y, x, p);
+
+ /* Skanky; use an otherwise-unused ImageData field
+ * to keep track of the max x,y used. Note that x and y are inverted.
+ */
+ if (y > id.x) {
+ id.x = y;
+ }
+ if (x > id.y) {
+ id.y = x;
+ }
+ }
+//xxx just give up; don't bother walking the rest of the image
+ } catch (IllegalArgumentException ex) {
+ System.out.println("bad pixels: order " + order +
+ ", dir " + dir +
+ ", w " + id.width +
+ ", h " + id.height +
+ ", x " + x +
+ ", y " + y);
+ throw ex;
+ }
+ } else {
+ order--;
+ int delta = 1 << order;
+ int nextX = x + delta;
+ int nextY = y + delta;
+
+ switch (dir) {
+ case HILBERT_DIR_E:
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S);
+ break;
+ case HILBERT_DIR_N:
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W);
+ break;
+ case HILBERT_DIR_S:
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E);
+ break;
+ case HILBERT_DIR_W:
+ hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S);
+ hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W);
+ hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N);
+ break;
+ default:
+ throw new RuntimeException("Unexpected Hilbert direction " +
+ dir);
+ }
+ }
+ }
+
+ private Point hilbertOrderData(ImageData id, byte pixData[]) {
+
+ int order = 0;
+ for (int n = 1; n < id.width; n *= 2) {
+ order++;
+ }
+ /* Skanky; use an otherwise-unused ImageData field
+ * to keep track of maxX.
+ */
+ Point p = new Point(0,0);
+ int oldIdX = id.x;
+ int oldIdY = id.y;
+ id.x = id.y = 0;
+ try {
+ hilbertWalk(id, new ByteArrayInputStream(pixData),
+ order, 0, 0, HILBERT_DIR_E);
+ p.x = id.x;
+ p.y = id.y;
+ } catch (IOException ex) {
+ System.err.println("Exception during hilbertWalk()");
+ p.x = id.height;
+ p.y = id.width;
+ }
+ id.x = oldIdX;
+ id.y = oldIdY;
+ return p;
+ }
+
+ private ImageData createHilbertHeapImage(byte pixData[]) {
+ int w, h;
+
+ // Pick an image size that the largest of heaps will fit into.
+ w = (int)Math.sqrt((double)((16 * 1024 * 1024)/8));
+
+ // Space-filling curves require a power-of-2 width.
+ w = nextPow2(w);
+ h = w;
+
+ // Create the heap image.
+ ImageData id = new ImageData(w, h, 8, mMapPalette);
+
+ // Copy the data into the image
+ //int maxX = zOrderData(id, pixData);
+ Point maxP = hilbertOrderData(id, pixData);
+
+ // update the max size to make it a round number once the zoom is applied
+ int factor = 100 / ZOOMS[mZoom.getSelectionIndex()];
+ if (factor != 1) {
+ int tmp = maxP.x % factor;
+ if (tmp != 0) {
+ maxP.x += factor - tmp;
+ }
+
+ tmp = maxP.y % factor;
+ if (tmp != 0) {
+ maxP.y += factor - tmp;
+ }
+ }
+
+ if (maxP.y < id.height) {
+ // Crop the image down to the interesting part.
+ id = new ImageData(id.width, maxP.y, id.depth, id.palette,
+ id.scanlinePad, id.data);
+ }
+
+ if (maxP.x < id.width) {
+ // crop the image again. A bit trickier this time.
+ ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette);
+
+ int[] buffer = new int[maxP.x];
+ for (int l = 0 ; l < id.height; l++) {
+ id.getPixels(0, l, maxP.x, buffer, 0);
+ croppedId.setPixels(0, l, maxP.x, buffer, 0);
+ }
+
+ id = croppedId;
+ }
+
+ // apply the zoom
+ if (factor != 1) {
+ id = id.scaledTo(id.width / factor, id.height / factor);
+ }
+
+ return id;
+ }
+
+ /**
+ * Convert the raw heap data to an image. We know we're running in
+ * the UI thread, so we can issue graphics commands directly.
+ *
+ * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html
+ *
+ * @param cd The client data
+ * @param mode The display mode. 0 = linear, 1 = hilbert.
+ * @param forceRedraw
+ */
+ private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) {
+ Image image;
+
+ byte[] pixData;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+ // no change, we return.
+ return;
+ }
+
+ pixData = getSerializedData();
+ }
+
+ if (pixData != null) {
+ ImageData id;
+ if (mode == 1) {
+ id = createHilbertHeapImage(pixData);
+ } else {
+ id = createLinearHeapImage(pixData, 200, mMapPalette);
+ }
+
+ image = new Image(mDisplay, id);
+ } else {
+ // Render a placeholder image.
+ int width, height;
+ if (mode == 1) {
+ width = height = PLACEHOLDER_HILBERT_SIZE;
+ } else {
+ width = PLACEHOLDER_LINEAR_H_SIZE;
+ height = PLACEHOLDER_LINEAR_V_SIZE;
+ }
+ image = new Image(mDisplay, width, height);
+ GC gc = new GC(image);
+ gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED));
+ gc.drawLine(0, 0, width-1, height-1);
+ gc.dispose();
+ gc = null;
+ }
+
+ // set the new image
+
+ if (mode == 1) {
+ if (mHilbertImage != null) {
+ mHilbertImage.dispose();
+ }
+
+ mHilbertImage = image;
+ mHilbertHeapImage.setImage(mHilbertImage);
+ mHilbertHeapImage.pack(true);
+ mHilbertBase.layout();
+ mHilbertBase.pack(true);
+ } else {
+ if (mLinearImage != null) {
+ mLinearImage.dispose();
+ }
+
+ mLinearImage = image;
+ mLinearHeapImage.setImage(mLinearImage);
+ mLinearHeapImage.pack(true);
+ mLinearBase.layout();
+ mLinearBase.pack(true);
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mHeapSummary);
+ }
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java
new file mode 100644
index 0000000..bcbf612
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Interface defining an image loader. jar app/lib and plugin have different packaging method
+ * so each implementation will be different.
+ * The implementation should implement at least one of the methods, and preferably both if possible.
+ *
+ */
+public interface IImageLoader {
+
+ /**
+ * Load an image from the resource from a filename
+ * @param filename
+ * @param display
+ */
+ public Image loadImage(String filename, Display display);
+
+ /**
+ * Load an ImageDescriptor from the resource from a filename
+ * @param filename
+ * @param display
+ */
+ public ImageDescriptor loadDescriptor(String filename, Display display);
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java
new file mode 100644
index 0000000..bf425d9
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.swt.dnd.Clipboard;
+
+/**
+ * An object listening to focus change in Table objects.<br>
+ * For application not relying on a RCP to provide menu changes based on focus,
+ * this class allows to get monitor the focus change of several Table widget
+ * and update the menu action accordingly.
+ */
+public interface ITableFocusListener {
+
+ public interface IFocusedTableActivator {
+ public void copy(Clipboard clipboard);
+
+ public void selectAll();
+ }
+
+ public void focusGained(IFocusedTableActivator activator);
+
+ public void focusLost(IFocusedTableActivator activator);
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java
new file mode 100644
index 0000000..d65978b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class ImageHelper {
+
+ /**
+ * Loads an image from a resource. This method used a class to locate the
+ * resources, and then load the filename from /images inside the resources.<br>
+ * Extra parameters allows for creation of a replacement image of the
+ * loading failed.
+ *
+ * @param loader the image loader used.
+ * @param display the Display object
+ * @param fileName the file name
+ * @param width optional width to create replacement Image. If -1, null be
+ * be returned if the loading fails.
+ * @param height optional height to create replacement Image. If -1, null be
+ * be returned if the loading fails.
+ * @param phColor optional color to create replacement Image. If null, Blue
+ * color will be used.
+ * @return a new Image or null if the loading failed and the optional
+ * replacement size was -1
+ */
+ public static Image loadImage(IImageLoader loader, Display display,
+ String fileName, int width, int height, Color phColor) {
+
+ Image img = null;
+ if (loader != null) {
+ img = loader.loadImage(fileName, display);
+ }
+
+ if (img == null) {
+ Log.w("ddms", "Couldn't load " + fileName);
+ // if we had the extra parameter to create replacement image then we
+ // create and return it.
+ if (width != -1 && height != -1) {
+ return createPlaceHolderArt(display, width, height,
+ phColor != null ? phColor : display
+ .getSystemColor(SWT.COLOR_BLUE));
+ }
+
+ // otherwise, just return null
+ return null;
+ }
+
+ return img;
+ }
+
+ /**
+ * Create place-holder art with the specified color.
+ */
+ public static Image createPlaceHolderArt(Display display, int width,
+ int height, Color color) {
+ Image img = new Image(display, width, height);
+ GC gc = new GC(img);
+ gc.setForeground(color);
+ gc.drawLine(0, 0, width, height);
+ gc.drawLine(0, height - 1, width, -1);
+ gc.dispose();
+ return img;
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java
new file mode 100644
index 0000000..76f2285
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.InputStream;
+
+/**
+ * Image loader for an normal standalone app.
+ */
+public class ImageLoader implements IImageLoader {
+
+ /** class used as reference to get the reources */
+ private Class<?> mClass;
+
+ /**
+ * Creates a loader for a specific class. The class allows java to figure
+ * out which .jar file to search for the image.
+ *
+ * @param theClass
+ */
+ public ImageLoader(Class<?> theClass) {
+ mClass = theClass;
+ }
+
+ public ImageDescriptor loadDescriptor(String filename, Display display) {
+ // we don't support ImageDescriptor
+ return null;
+ }
+
+ public Image loadImage(String filename, Display display) {
+
+ String tmp = "/images/" + filename;
+ InputStream imageStream = mClass.getResourceAsStream(tmp);
+
+ if (imageStream != null) {
+ Image img = new Image(display, imageStream);
+ if (img == null)
+ throw new NullPointerException("couldn't load " + tmp);
+ return img;
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java
new file mode 100644
index 0000000..72cbb4a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+/**
+ * Display client info in a two-column format.
+ */
+public class InfoPanel extends TablePanel {
+ private Table mTable;
+ private TableColumn mCol2;
+
+ private static final String mLabels[] = {
+ "DDM-aware?",
+ "App description:",
+ "VM version:",
+ "Process ID:",
+ };
+ private static final int ENT_DDM_AWARE = 0;
+ private static final int ENT_APP_DESCR = 1;
+ private static final int ENT_VM_VERSION = 2;
+ private static final int ENT_PROCESS_ID = 3;
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mTable.setHeaderVisible(false);
+ mTable.setLinesVisible(false);
+
+ TableColumn col1 = new TableColumn(mTable, SWT.RIGHT);
+ col1.setText("name");
+ mCol2 = new TableColumn(mTable, SWT.LEFT);
+ mCol2.setText("PlaceHolderContentForWidth");
+
+ TableItem item;
+ for (int i = 0; i < mLabels.length; i++) {
+ item = new TableItem(mTable, SWT.NONE);
+ item.setText(0, mLabels[i]);
+ item.setText(1, "-");
+ }
+
+ col1.pack();
+ mCol2.pack();
+
+ return mTable;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTable.setFocus();
+ }
+
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) {
+ if (mTable.isDisposed())
+ return;
+
+ mTable.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ clientSelected();
+ }
+ });
+ }
+ }
+ }
+
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}
+ */
+ @Override
+ public void clientSelected() {
+ if (mTable.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ if (client == null) {
+ for (int i = 0; i < mLabels.length; i++) {
+ TableItem item = mTable.getItem(i);
+ item.setText(1, "-");
+ }
+ } else {
+ TableItem item;
+ String clientDescription, vmIdentifier, isDdmAware,
+ pid;
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ clientDescription = (cd.getClientDescription() != null) ?
+ cd.getClientDescription() : "?";
+ vmIdentifier = (cd.getVmIdentifier() != null) ?
+ cd.getVmIdentifier() : "?";
+ isDdmAware = cd.isDdmAware() ?
+ "yes" : "no";
+ pid = (cd.getPid() != 0) ?
+ String.valueOf(cd.getPid()) : "?";
+ }
+
+ item = mTable.getItem(ENT_APP_DESCR);
+ item.setText(1, clientDescription);
+ item = mTable.getItem(ENT_VM_VERSION);
+ item.setText(1, vmIdentifier);
+ item = mTable.getItem(ENT_DDM_AWARE);
+ item.setText(1, isDdmAware);
+ item = mTable.getItem(ENT_PROCESS_ID);
+ item.setText(1, pid);
+ }
+
+ mCol2.pack();
+
+ //Log.i("ddms", "InfoPanel: changed " + client);
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mTable);
+ }
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java
new file mode 100644
index 0000000..46461bf
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java
@@ -0,0 +1,1633 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+import com.android.ddmuilib.annotation.WorkerThread;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * Panel with native heap information.
+ */
+public final class NativeHeapPanel extends BaseHeapPanel {
+
+ /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+ * Native+1 at least. We also need 2 more entries for free area and expansion area. */
+ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+ private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+ private static final PaletteData mMapPalette = createPalette();
+
+ private static final int ALLOC_DISPLAY_ALL = 0;
+ private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1;
+ private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2;
+
+ private Display mDisplay;
+
+ private Composite mBase;
+
+ private Label mUpdateStatus;
+
+ /** combo giving choice of what to display: all, pre-zygote, post-zygote */
+ private Combo mAllocDisplayCombo;
+
+ private Button mFullUpdateButton;
+
+ // see CreateControl()
+ //private Button mDiffUpdateButton;
+
+ private Combo mDisplayModeCombo;
+
+ /** stack composite for mode (1-2) & 3 */
+ private Composite mTopStackComposite;
+
+ private StackLayout mTopStackLayout;
+
+ /** stack composite for mode 1 & 2 */
+ private Composite mAllocationStackComposite;
+
+ private StackLayout mAllocationStackLayout;
+
+ /** top level container for mode 1 & 2 */
+ private Composite mTableModeControl;
+
+ /** top level object for the allocation mode */
+ private Control mAllocationModeTop;
+
+ /** top level for the library mode */
+ private Control mLibraryModeTopControl;
+
+ /** composite for page UI and total memory display */
+ private Composite mPageUIComposite;
+
+ private Label mTotalMemoryLabel;
+
+ private Label mPageLabel;
+
+ private Button mPageNextButton;
+
+ private Button mPagePreviousButton;
+
+ private Table mAllocationTable;
+
+ private Table mLibraryTable;
+
+ private Table mLibraryAllocationTable;
+
+ private Table mDetailTable;
+
+ private Label mImage;
+
+ private int mAllocDisplayMode = ALLOC_DISPLAY_ALL;
+
+ /**
+ * pointer to current stackcall thread computation in order to quit it if
+ * required (new update requested)
+ */
+ private StackCallThread mStackCallThread;
+
+ /** Current Library Allocation table fill thread. killed if selection changes */
+ private FillTableThread mFillTableThread;
+
+ /**
+ * current client data. Used to access the malloc info when switching pages
+ * or selecting allocation to show stack call
+ */
+ private ClientData mClientData;
+
+ /**
+ * client data from a previous display. used when asking for an "update & diff"
+ */
+ private ClientData mBackUpClientData;
+
+ /** list of NativeAllocationInfo objects filled with the list from ClientData */
+ private final ArrayList<NativeAllocationInfo> mAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** list of the {@link NativeAllocationInfo} being displayed based on the selection
+ * of {@link #mAllocDisplayCombo}.
+ */
+ private final ArrayList<NativeAllocationInfo> mDisplayedAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */
+ private final ArrayList<NativeAllocationInfo> mBackUpAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ /** back up of the total memory, used when doing an "update & diff" */
+ private int mBackUpTotalMemory;
+
+ private int mCurrentPage = 0;
+
+ private int mPageCount = 0;
+
+ /**
+ * list of allocation per Library. This is created from the list of
+ * NativeAllocationInfo objects that is stored in the ClientData object. Since we
+ * don't keep this list around, it is recomputed everytime the client
+ * changes.
+ */
+ private final ArrayList<LibraryAllocations> mLibraryAllocations =
+ new ArrayList<LibraryAllocations>();
+
+ /* args to setUpdateStatus() */
+ private static final int NOT_SELECTED = 0;
+
+ private static final int NOT_ENABLED = 1;
+
+ private static final int ENABLED = 2;
+
+ private static final int DISPLAY_PER_PAGE = 20;
+
+ private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$
+ private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$
+ private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$
+ private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$
+ private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$
+ private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$
+ private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$
+ private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$
+
+ /** static formatter object to format all numbers as #,### */
+ private static DecimalFormat sFormatter;
+ static {
+ sFormatter = (DecimalFormat)NumberFormat.getInstance();
+ if (sFormatter != null)
+ sFormatter = new DecimalFormat("#,###");
+ else
+ sFormatter.applyPattern("#,###");
+ }
+
+
+ /**
+ * caching mechanism to avoid recomputing the backtrace for a particular
+ * address several times.
+ */
+ private HashMap<Long, NativeStackCallInfo> mSourceCache =
+ new HashMap<Long,NativeStackCallInfo>();
+ private long mTotalSize;
+ private Button mSaveButton;
+ private Button mSymbolsButton;
+
+ /**
+ * thread class to convert the address call into method, file and line
+ * number in the background.
+ */
+ private class StackCallThread extends BackgroundThread {
+ private ClientData mClientData;
+
+ public StackCallThread(ClientData cd) {
+ mClientData = cd;
+ }
+
+ public ClientData getClientData() {
+ return mClientData;
+ }
+
+ @Override
+ public void run() {
+ // loop through all the NativeAllocationInfo and init them
+ Iterator<NativeAllocationInfo> iter = mAllocations.iterator();
+ int total = mAllocations.size();
+ int count = 0;
+ while (iter.hasNext()) {
+
+ if (isQuitting())
+ return;
+
+ NativeAllocationInfo info = iter.next();
+ if (info.isStackCallResolved() == false) {
+ final Long[] list = info.getStackCallAddresses();
+ final int size = list.length;
+
+ ArrayList<NativeStackCallInfo> resolvedStackCall =
+ new ArrayList<NativeStackCallInfo>();
+
+ for (int i = 0; i < size; i++) {
+ long addr = list[i];
+
+ // first check if the addr has already been converted.
+ NativeStackCallInfo source = mSourceCache.get(addr);
+
+ // if not we convert it
+ if (source == null) {
+ source = sourceForAddr(addr);
+ mSourceCache.put(addr, source);
+ }
+
+ resolvedStackCall.add(source);
+ }
+
+ info.setResolvedStackCall(resolvedStackCall);
+ }
+ // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless
+ // we reach total, since we also do it after the loop
+ // (only an issue in case we have a perfect number of page)
+ count++;
+ if ((count % DISPLAY_PER_PAGE) == 0 && count != total) {
+ if (updateNHAllocationStackCalls(mClientData, count) == false) {
+ // looks like the app is quitting, so we just
+ // stopped the thread
+ return;
+ }
+ }
+ }
+
+ updateNHAllocationStackCalls(mClientData, count);
+ }
+
+ private NativeStackCallInfo sourceForAddr(long addr) {
+ NativeLibraryMapInfo library = getLibraryFor(addr);
+
+ if (library != null) {
+
+ Addr2Line process = Addr2Line.getProcess(library.getLibraryName());
+ if (process != null) {
+ // remove the base of the library address
+ long value = addr - library.getStartAddress();
+ NativeStackCallInfo info = process.getAddress(value);
+ if (info != null) {
+ return info;
+ }
+ }
+ }
+
+ return new NativeStackCallInfo(library != null ? library.getLibraryName() : null,
+ Long.toHexString(addr), "");
+ }
+
+ private NativeLibraryMapInfo getLibraryFor(long addr) {
+ Iterator<NativeLibraryMapInfo> it = mClientData.getNativeLibraryMapInfo();
+
+ while (it.hasNext()) {
+ NativeLibraryMapInfo info = it.next();
+
+ if (info.isWithinLibrary(addr)) {
+ return info;
+ }
+ }
+
+ Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
+ return null;
+ }
+
+ /**
+ * update the Native Heap panel with the amount of allocation for which the
+ * stack call has been computed. This is called from a non UI thread, but
+ * will be executed in the UI thread.
+ *
+ * @param count the amount of allocation
+ * @return false if the display was disposed and the update couldn't happen
+ */
+ private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) {
+ if (mDisplay.isDisposed() == false) {
+ mDisplay.asyncExec(new Runnable() {
+ public void run() {
+ updateAllocationStackCalls(clientData, count);
+ }
+ });
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private class FillTableThread extends BackgroundThread {
+ private LibraryAllocations mLibAlloc;
+
+ private int mMax;
+
+ public FillTableThread(LibraryAllocations liballoc, int m) {
+ mLibAlloc = liballoc;
+ mMax = m;
+ }
+
+ @Override
+ public void run() {
+ for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) {
+ updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10);
+ }
+ }
+
+ /**
+ * updates the library allocation table in the Native Heap panel. This is
+ * called from a non UI thread, but will be executed in the UI thread.
+ *
+ * @param liballoc the current library allocation object being displayed
+ * @param start start index of items that need to be displayed
+ * @param end end index of the items that need to be displayed
+ */
+ private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc,
+ final int start, final int end) {
+ if (mDisplay.isDisposed() == false) {
+ mDisplay.asyncExec(new Runnable() {
+ public void run() {
+ updateLibraryAllocationTable(libAlloc, start, end);
+ }
+ });
+ }
+
+ }
+ }
+
+ /** class to aggregate allocations per library */
+ public static class LibraryAllocations {
+ private String mLibrary;
+
+ private final ArrayList<NativeAllocationInfo> mLibAllocations =
+ new ArrayList<NativeAllocationInfo>();
+
+ private int mSize;
+
+ private int mCount;
+
+ /** construct the aggregate object for a library */
+ public LibraryAllocations(final String lib) {
+ mLibrary = lib;
+ }
+
+ /** get the library name */
+ public String getLibrary() {
+ return mLibrary;
+ }
+
+ /** add a NativeAllocationInfo object to this aggregate object */
+ public void addAllocation(NativeAllocationInfo info) {
+ mLibAllocations.add(info);
+ }
+
+ /** get an iterator on the NativeAllocationInfo objects */
+ public Iterator<NativeAllocationInfo> getAllocations() {
+ return mLibAllocations.iterator();
+ }
+
+ /** get a NativeAllocationInfo object by index */
+ public NativeAllocationInfo getAllocation(int index) {
+ return mLibAllocations.get(index);
+ }
+
+ /** returns the NativeAllocationInfo object count */
+ public int getAllocationSize() {
+ return mLibAllocations.size();
+ }
+
+ /** returns the total allocation size */
+ public int getSize() {
+ return mSize;
+ }
+
+ /** returns the number of allocations */
+ public int getCount() {
+ return mCount;
+ }
+
+ /**
+ * compute the allocation count and size for allocation objects added
+ * through <code>addAllocation()</code>, and sort the objects by
+ * total allocation size.
+ */
+ public void computeAllocationSizeAndCount() {
+ mSize = 0;
+ mCount = 0;
+ for (NativeAllocationInfo info : mLibAllocations) {
+ mCount += info.getAllocationCount();
+ mSize += info.getAllocationCount() * info.getSize();
+ }
+ Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() {
+ public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) {
+ return o2.getAllocationCount() * o2.getSize() -
+ o1.getAllocationCount() * o1.getSize();
+ }
+ });
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+
+ mDisplay = parent.getDisplay();
+
+ mBase = new Composite(parent, SWT.NONE);
+ GridLayout gl = new GridLayout(1, false);
+ gl.horizontalSpacing = 0;
+ gl.verticalSpacing = 0;
+ mBase.setLayout(gl);
+ mBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // composite for <update btn> <status>
+ Composite tmp = new Composite(mBase, SWT.NONE);
+ tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ tmp.setLayout(gl = new GridLayout(2, false));
+ gl.marginWidth = gl.marginHeight = 0;
+
+ mFullUpdateButton = new Button(tmp, SWT.NONE);
+ mFullUpdateButton.setText("Full Update");
+ mFullUpdateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mBackUpClientData = null;
+ mDisplayModeCombo.setEnabled(false);
+ mSaveButton.setEnabled(false);
+ emptyTables();
+ // if we already have a stack call computation for this
+ // client
+ // we stop it
+ if (mStackCallThread != null &&
+ mStackCallThread.getClientData() == mClientData) {
+ mStackCallThread.quit();
+ mStackCallThread = null;
+ }
+ mLibraryAllocations.clear();
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestNativeHeapInformation();
+ }
+ }
+ });
+
+ mUpdateStatus = new Label(tmp, SWT.NONE);
+ mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // top layout for the combos and oter controls on the right.
+ Composite top_layout = new Composite(mBase, SWT.NONE);
+ top_layout.setLayout(gl = new GridLayout(4, false));
+ gl.marginWidth = gl.marginHeight = 0;
+
+ new Label(top_layout, SWT.NONE).setText("Show:");
+
+ mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mAllocDisplayCombo.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mAllocDisplayCombo.add("All Allocations");
+ mAllocDisplayCombo.add("Pre-Zygote Allocations");
+ mAllocDisplayCombo.add("Zygote Child Allocations (Z)");
+ mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onAllocDisplayChange();
+ }
+ });
+ mAllocDisplayCombo.select(0);
+
+ // separator
+ Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL);
+ GridData gd;
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+ gd.verticalSpan = 2;
+
+ mSaveButton = new Button(top_layout, SWT.PUSH);
+ mSaveButton.setText("Save...");
+ mSaveButton.setEnabled(false);
+ mSaveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Save Allocations");
+ fileDialog.setFileName("allocations.txt");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ saveAllocations(fileName);
+ }
+ }
+ });
+
+ /*
+ * TODO: either fix the diff mechanism or remove it altogether.
+ mDiffUpdateButton = new Button(top_layout, SWT.NONE);
+ mDiffUpdateButton.setText("Update && Diff");
+ mDiffUpdateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // since this is an update and diff, we need to store the
+ // current list
+ // of mallocs
+ mBackUpAllocations.clear();
+ mBackUpAllocations.addAll(mAllocations);
+ mBackUpClientData = mClientData;
+ mBackUpTotalMemory = mClientData.getTotalNativeMemory();
+
+ mDisplayModeCombo.setEnabled(false);
+ emptyTables();
+ // if we already have a stack call computation for this
+ // client
+ // we stop it
+ if (mStackCallThread != null &&
+ mStackCallThread.getClientData() == mClientData) {
+ mStackCallThread.quit();
+ mStackCallThread = null;
+ }
+ mLibraryAllocations.clear();
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestNativeHeapInformation();
+ }
+ }
+ });
+ */
+
+ Label l = new Label(top_layout, SWT.NONE);
+ l.setText("Display:");
+
+ mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mDisplayModeCombo.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" });
+ mDisplayModeCombo.select(0);
+ mDisplayModeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ switchDisplayMode();
+ }
+ });
+ mDisplayModeCombo.setEnabled(false);
+
+ mSymbolsButton = new Button(top_layout, SWT.PUSH);
+ mSymbolsButton.setText("Load Symbols");
+ mSymbolsButton.setEnabled(false);
+
+
+ // create a composite that will contains the actual content composites,
+ // in stack mode layout.
+ // This top level composite contains 2 other composites.
+ // * one for both Allocations and Libraries mode
+ // * one for flat mode (which is gone for now)
+
+ mTopStackComposite = new Composite(mBase, SWT.NONE);
+ mTopStackComposite.setLayout(mTopStackLayout = new StackLayout());
+ mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // create 1st and 2nd modes
+ createTableDisplay(mTopStackComposite);
+
+ mTopStackLayout.topControl = mTableModeControl;
+ mTopStackComposite.layout();
+
+ setUpdateStatus(NOT_SELECTED);
+
+ // Work in progress
+ // TODO add image display of native heap.
+ //mImage = new Label(mBase, SWT.NONE);
+
+ mBase.pack();
+
+ return mBase;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ // TODO
+ }
+
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) {
+ if (mBase.isDisposed())
+ return;
+
+ mBase.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ clientSelected();
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mBase.isDisposed())
+ return;
+
+ Client client = getCurrentClient();
+
+ mDisplayModeCombo.setEnabled(false);
+ emptyTables();
+
+ Log.d("ddms", "NativeHeapPanel: changed " + client);
+
+ if (client != null) {
+ ClientData cd = client.getClientData();
+ mClientData = cd;
+
+ // if (cd.getShowHeapUpdates())
+ setUpdateStatus(ENABLED);
+ // else
+ // setUpdateStatus(NOT_ENABLED);
+
+ initAllocationDisplay();
+
+ //renderBitmap(cd);
+ } else {
+ mClientData = null;
+ setUpdateStatus(NOT_SELECTED);
+ }
+
+ mBase.pack();
+ }
+
+ /**
+ * Update the UI with the newly compute stack calls, unless the UI switched
+ * to a different client.
+ *
+ * @param cd the ClientData for which the stack call are being computed.
+ * @param count the current count of allocations for which the stack calls
+ * have been computed.
+ */
+ @WorkerThread
+ public void updateAllocationStackCalls(ClientData cd, int count) {
+ // we have to check that the panel still shows the same clientdata than
+ // the thread is computing for.
+ if (cd == mClientData) {
+
+ int total = mAllocations.size();
+
+ if (count == total) {
+ // we're done: do something
+ mDisplayModeCombo.setEnabled(true);
+ mSaveButton.setEnabled(true);
+
+ mStackCallThread = null;
+ } else {
+ // work in progress, update the progress bar.
+// mUiThread.setStatusLine("Computing stack call: " + count
+// + "/" + total);
+ }
+
+ // FIXME: attempt to only update when needed.
+ // Because the number of pages is not related to mAllocations.size() anymore
+ // due to pre-zygote/post-zygote display, update all the time.
+ // At some point we should remove the pages anyway, since it's getting computed
+ // really fast now.
+// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count
+// || (count == total && mCurrentPage == mPageCount - 1)) {
+ try {
+ // get the current selection of the allocation
+ int index = mAllocationTable.getSelectionIndex();
+ NativeAllocationInfo info = null;
+
+ if (index != -1) {
+ info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData();
+ }
+
+ // empty the table
+ emptyTables();
+
+ // fill it again
+ fillAllocationTable();
+
+ // reselect
+ mAllocationTable.setSelection(index);
+
+ // display detail table if needed
+ if (info != null) {
+ fillDetailTable(info);
+ }
+ } catch (SWTException e) {
+ if (mAllocationTable.isDisposed()) {
+ // looks like the table is disposed. Let's ignore it.
+ } else {
+ throw e;
+ }
+ }
+
+ } else {
+ // old client still running. doesn't really matter.
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mAllocationTable);
+ addTableToFocusListener(mLibraryTable);
+ addTableToFocusListener(mLibraryAllocationTable);
+ addTableToFocusListener(mDetailTable);
+ }
+
+ protected void onAllocDisplayChange() {
+ mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex();
+
+ // create the new list
+ updateAllocDisplayList();
+
+ updateTotalMemoryDisplay();
+
+ // reset the ui.
+ mCurrentPage = 0;
+ updatePageUI();
+ switchDisplayMode();
+ }
+
+ private void updateAllocDisplayList() {
+ mTotalSize = 0;
+ mDisplayedAllocations.clear();
+ for (NativeAllocationInfo info : mAllocations) {
+ if (mAllocDisplayMode == ALLOC_DISPLAY_ALL ||
+ (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) {
+ mDisplayedAllocations.add(info);
+ mTotalSize += info.getSize() * info.getAllocationCount();
+ } else {
+ // skip this item
+ continue;
+ }
+ }
+
+ int count = mDisplayedAllocations.size();
+
+ mPageCount = count / DISPLAY_PER_PAGE;
+
+ // need to add a page for the rest of the div
+ if ((count % DISPLAY_PER_PAGE) > 0) {
+ mPageCount++;
+ }
+ }
+
+ private void updateTotalMemoryDisplay() {
+ switch (mAllocDisplayMode) {
+ case ALLOC_DISPLAY_ALL:
+ mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ case ALLOC_DISPLAY_PRE_ZYGOTE:
+ mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ case ALLOC_DISPLAY_POST_ZYGOTE:
+ mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes",
+ sFormatter.format(mTotalSize)));
+ break;
+ }
+ }
+
+
+ private void switchDisplayMode() {
+ switch (mDisplayModeCombo.getSelectionIndex()) {
+ case 0: {// allocations
+ mTopStackLayout.topControl = mTableModeControl;
+ mAllocationStackLayout.topControl = mAllocationModeTop;
+ mAllocationStackComposite.layout();
+ mTopStackComposite.layout();
+ emptyTables();
+ fillAllocationTable();
+ }
+ break;
+ case 1: {// libraries
+ mTopStackLayout.topControl = mTableModeControl;
+ mAllocationStackLayout.topControl = mLibraryModeTopControl;
+ mAllocationStackComposite.layout();
+ mTopStackComposite.layout();
+ emptyTables();
+ fillLibraryTable();
+ }
+ break;
+ }
+ }
+
+ private void initAllocationDisplay() {
+ mAllocations.clear();
+ mAllocations.addAll(mClientData.getNativeAllocationList());
+
+ updateAllocDisplayList();
+
+ // if we have a previous clientdata and it matches the current one. we
+ // do a diff between the new list and the old one.
+ if (mBackUpClientData != null && mBackUpClientData == mClientData) {
+
+ ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>();
+
+ // we go through the list of NativeAllocationInfo in the new list and check if
+ // there's one with the same exact data (size, allocation, count and
+ // stackcall addresses) in the old list.
+ // if we don't find any, we add it to the "add" list
+ for (NativeAllocationInfo mi : mAllocations) {
+ boolean found = false;
+ for (NativeAllocationInfo old_mi : mBackUpAllocations) {
+ if (mi.equals(old_mi)) {
+ found = true;
+ break;
+ }
+ }
+ if (found == false) {
+ add.add(mi);
+ }
+ }
+
+ // put the result in mAllocations
+ mAllocations.clear();
+ mAllocations.addAll(add);
+
+ // display the difference in memory usage. This is computed
+ // calculating the memory usage of the objects in mAllocations.
+ int count = 0;
+ for (NativeAllocationInfo allocInfo : mAllocations) {
+ count += allocInfo.getSize() * allocInfo.getAllocationCount();
+ }
+
+ mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes",
+ sFormatter.format(count)));
+ }
+ else {
+ // display the full memory usage
+ updateTotalMemoryDisplay();
+ //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0);
+ }
+ mTotalMemoryLabel.pack();
+
+ // update the page ui
+ mDisplayModeCombo.select(0);
+
+ mLibraryAllocations.clear();
+
+ // reset to first page
+ mCurrentPage = 0;
+
+ // update the label
+ updatePageUI();
+
+ // now fill the allocation Table with the current page
+ switchDisplayMode();
+
+ // start the thread to compute the stack calls
+ if (mAllocations.size() > 0) {
+ mStackCallThread = new StackCallThread(mClientData);
+ mStackCallThread.start();
+ }
+ }
+
+ private void updatePageUI() {
+
+ // set the label and pack to update the layout, otherwise
+ // the label will be cut off if the new size is bigger
+ if (mPageCount == 0) {
+ mPageLabel.setText("0 of 0 allocations.");
+ } else {
+ StringBuffer buffer = new StringBuffer();
+ // get our starting index
+ int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1;
+ // end index, taking into account the last page can be half full
+ int count = mDisplayedAllocations.size();
+ int end = Math.min(start + DISPLAY_PER_PAGE - 1, count);
+ buffer.append(sFormatter.format(start));
+ buffer.append(" - ");
+ buffer.append(sFormatter.format(end));
+ buffer.append(" of ");
+ buffer.append(sFormatter.format(count));
+ buffer.append(" allocations.");
+ mPageLabel.setText(buffer.toString());
+ }
+
+ // handle the button enabled state.
+ mPagePreviousButton.setEnabled(mCurrentPage > 0);
+ // reminder: mCurrentPage starts at 0.
+ mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1);
+
+ mPageLabel.pack();
+ mPageUIComposite.pack();
+
+ }
+
+ private void fillAllocationTable() {
+ // get the count
+ int count = mDisplayedAllocations.size();
+
+ // get our starting index
+ int start = mCurrentPage * DISPLAY_PER_PAGE;
+
+ // loop for DISPLAY_PER_PAGE or till we reach count
+ int end = start + DISPLAY_PER_PAGE;
+
+ for (int i = start; i < end && i < count; i++) {
+ NativeAllocationInfo info = mDisplayedAllocations.get(i);
+
+ TableItem item = null;
+
+ if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) {
+ item = new TableItem(mAllocationTable, SWT.NONE);
+ item.setText(0, (info.isZygoteChild() ? "Z " : "") +
+ sFormatter.format(info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+ } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) {
+ item = new TableItem(mAllocationTable, SWT.NONE);
+ item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+ } else {
+ // skip this item
+ continue;
+ }
+
+ item.setData(info);
+
+ NativeStackCallInfo bti = info.getRelevantStackCallInfo();
+ if (bti != null) {
+ String lib = bti.getLibraryName();
+ String method = bti.getMethodName();
+ String source = bti.getSourceFile();
+ if (lib != null)
+ item.setText(3, lib);
+ if (method != null)
+ item.setText(4, method);
+ if (source != null)
+ item.setText(5, source);
+ }
+ }
+ }
+
+ private void fillLibraryTable() {
+ // fill the library table
+ sortAllocationsPerLibrary();
+
+ for (LibraryAllocations liballoc : mLibraryAllocations) {
+ if (liballoc != null) {
+ TableItem item = new TableItem(mLibraryTable, SWT.NONE);
+ String lib = liballoc.getLibrary();
+ item.setText(0, lib != null ? lib : "");
+ item.setText(1, sFormatter.format(liballoc.getSize()));
+ item.setText(2, sFormatter.format(liballoc.getCount()));
+ }
+ }
+ }
+
+ private void fillLibraryAllocationTable() {
+ mLibraryAllocationTable.removeAll();
+ mDetailTable.removeAll();
+ int index = mLibraryTable.getSelectionIndex();
+ if (index != -1) {
+ LibraryAllocations liballoc = mLibraryAllocations.get(index);
+ // start a thread that will fill table 10 at a time to keep the ui
+ // responsive, but first we kill the previous one if there was one
+ if (mFillTableThread != null) {
+ mFillTableThread.quit();
+ }
+ mFillTableThread = new FillTableThread(liballoc,
+ liballoc.getAllocationSize());
+ mFillTableThread.start();
+ }
+ }
+
+ public void updateLibraryAllocationTable(LibraryAllocations liballoc,
+ int start, int end) {
+ try {
+ if (mLibraryTable.isDisposed() == false) {
+ int index = mLibraryTable.getSelectionIndex();
+ if (index != -1) {
+ LibraryAllocations newliballoc = mLibraryAllocations.get(
+ index);
+ if (newliballoc == liballoc) {
+ int count = liballoc.getAllocationSize();
+ for (int i = start; i < end && i < count; i++) {
+ NativeAllocationInfo info = liballoc.getAllocation(i);
+
+ TableItem item = new TableItem(
+ mLibraryAllocationTable, SWT.NONE);
+ item.setText(0, sFormatter.format(
+ info.getSize() * info.getAllocationCount()));
+ item.setText(1, sFormatter.format(info.getAllocationCount()));
+ item.setText(2, sFormatter.format(info.getSize()));
+
+ NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo();
+ if (stackCallInfo != null) {
+ item.setText(3, stackCallInfo.getMethodName());
+ }
+ }
+ } else {
+ // we should quit the thread
+ if (mFillTableThread != null) {
+ mFillTableThread.quit();
+ mFillTableThread = null;
+ }
+ }
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("ddms", "error when updating the library allocation table");
+ }
+ }
+
+ private void fillDetailTable(final NativeAllocationInfo mi) {
+ mDetailTable.removeAll();
+ mDetailTable.setRedraw(false);
+
+ try {
+ // populate the detail Table with the back trace
+ Long[] addresses = mi.getStackCallAddresses();
+ NativeStackCallInfo[] resolvedStackCall = mi.getResolvedStackCall();
+
+ if (resolvedStackCall == null) {
+ return;
+ }
+
+ for (int i = 0 ; i < resolvedStackCall.length ; i++) {
+ if (addresses[i] == null || addresses[i].longValue() == 0) {
+ continue;
+ }
+
+ long addr = addresses[i].longValue();
+ NativeStackCallInfo source = resolvedStackCall[i];
+
+ TableItem item = new TableItem(mDetailTable, SWT.NONE);
+ item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$
+
+ String libraryName = source.getLibraryName();
+ String methodName = source.getMethodName();
+ String sourceFile = source.getSourceFile();
+ int lineNumber = source.getLineNumber();
+
+ if (libraryName != null)
+ item.setText(1, libraryName);
+ if (methodName != null)
+ item.setText(2, methodName);
+ if (sourceFile != null)
+ item.setText(3, sourceFile);
+ if (lineNumber != -1)
+ item.setText(4, Integer.toString(lineNumber));
+ }
+ } finally {
+ mDetailTable.setRedraw(true);
+ }
+ }
+
+ /*
+ * Are updates enabled?
+ */
+ private void setUpdateStatus(int status) {
+ switch (status) {
+ case NOT_SELECTED:
+ mUpdateStatus.setText("Select a client to see heap info");
+ mAllocDisplayCombo.setEnabled(false);
+ mFullUpdateButton.setEnabled(false);
+ //mDiffUpdateButton.setEnabled(false);
+ break;
+ case NOT_ENABLED:
+ mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client");
+ mAllocDisplayCombo.setEnabled(false);
+ mFullUpdateButton.setEnabled(false);
+ //mDiffUpdateButton.setEnabled(false);
+ break;
+ case ENABLED:
+ mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data");
+ mAllocDisplayCombo.setEnabled(true);
+ mFullUpdateButton.setEnabled(true);
+ //mDiffUpdateButton.setEnabled(true);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
+ mUpdateStatus.pack();
+ }
+
+ /**
+ * Create the Table display. This includes a "detail" Table in the bottom
+ * half and 2 modes in the top half: allocation Table and
+ * library+allocations Tables.
+ *
+ * @param base the top parent to create the display into
+ */
+ private void createTableDisplay(Composite base) {
+ final int minPanelWidth = 60;
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ // top level composite for mode 1 & 2
+ mTableModeControl = new Composite(base, SWT.NONE);
+ GridLayout gl = new GridLayout(1, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ mTableModeControl.setLayout(gl);
+ mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE);
+ mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTotalMemoryLabel.setText("Total Memory: 0 Bytes");
+
+ // the top half of these modes is dynamic
+
+ final Composite sash_composite = new Composite(mTableModeControl,
+ SWT.NONE);
+ sash_composite.setLayout(new FormLayout());
+ sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // create the stacked composite
+ mAllocationStackComposite = new Composite(sash_composite, SWT.NONE);
+ mAllocationStackLayout = new StackLayout();
+ mAllocationStackComposite.setLayout(mAllocationStackLayout);
+ mAllocationStackComposite.setLayoutData(new GridData(
+ GridData.FILL_BOTH));
+
+ // create the top half for mode 1
+ createAllocationTopHalf(mAllocationStackComposite);
+
+ // create the top half for mode 2
+ createLibraryTopHalf(mAllocationStackComposite);
+
+ final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL);
+
+ // bottom half of these modes is the same: detail table
+ createDetailTable(sash_composite);
+
+ // init value for stack
+ mAllocationStackLayout.topControl = mAllocationModeTop;
+
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(mTotalMemoryLabel, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mAllocationStackComposite.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) {
+ sashData.top = new FormAttachment(0,
+ prefs.getInt(PREFS_ALLOCATION_SASH));
+ } else {
+ sashData.top = new FormAttachment(50, 0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mDetailTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = sash_composite.getClientArea();
+ int bottom = panelRect.height - sashRect.height - minPanelWidth;
+ e.y = Math.max(Math.min(e.y, bottom), minPanelWidth);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ prefs.setValue(PREFS_ALLOCATION_SASH, e.y);
+ sash_composite.layout();
+ }
+ }
+ });
+ }
+
+ private void createDetailTable(Composite base) {
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+ mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mDetailTable.setHeaderVisible(true);
+ mDetailTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT,
+ "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT,
+ "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$
+ }
+
+ private void createAllocationTopHalf(Composite b) {
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ Composite base = new Composite(b, SWT.NONE);
+ mAllocationModeTop = base;
+ GridLayout gl = new GridLayout(1, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ gl.verticalSpacing = 0;
+ base.setLayout(gl);
+ base.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // horizontal layout for memory total and pages UI
+ mPageUIComposite = new Composite(base, SWT.NONE);
+ mPageUIComposite.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_BEGINNING));
+ gl = new GridLayout(3, false);
+ gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+ gl.horizontalSpacing = 0;
+ mPageUIComposite.setLayout(gl);
+
+ // Page UI
+ mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE);
+ mPagePreviousButton.setText("<");
+ mPagePreviousButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mCurrentPage--;
+ updatePageUI();
+ emptyTables();
+ fillAllocationTable();
+ }
+ });
+
+ mPageNextButton = new Button(mPageUIComposite, SWT.NONE);
+ mPageNextButton.setText(">");
+ mPageNextButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mCurrentPage++;
+ updatePageUI();
+ emptyTables();
+ fillAllocationTable();
+ }
+ });
+
+ mPageLabel = new Label(mPageUIComposite, SWT.NONE);
+ mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ updatePageUI();
+
+ mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+ mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mAllocationTable.setHeaderVisible(true);
+ mAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT,
+ "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT,
+ "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT,
+ "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT,
+ "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$
+
+ mAllocationTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the selection index
+ int index = mAllocationTable.getSelectionIndex();
+ TableItem item = mAllocationTable.getItem(index);
+ if (item != null && item.getData() instanceof NativeAllocationInfo) {
+ fillDetailTable((NativeAllocationInfo)item.getData());
+ }
+ }
+ });
+ }
+
+ private void createLibraryTopHalf(Composite base) {
+ final int minPanelWidth = 60;
+
+ final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+ // create a composite that'll contain 2 tables horizontally
+ final Composite top = new Composite(base, SWT.NONE);
+ mLibraryModeTopControl = top;
+ top.setLayout(new FormLayout());
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // first table: library
+ mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+ mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mLibraryTable.setHeaderVisible(true);
+ mLibraryTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT,
+ "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT,
+ "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$
+
+ mLibraryTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ fillLibraryAllocationTable();
+ }
+ });
+
+ final Sash sash = new Sash(top, SWT.VERTICAL);
+
+ // 2nd table: allocation per library
+ mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+ mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mLibraryAllocationTable.setHeaderVisible(true);
+ mLibraryAllocationTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Total",
+ SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Count",
+ SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Size",
+ SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$
+ TableHelper.createTableColumn(mLibraryAllocationTable, "Method",
+ SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$
+
+ mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the index of the selection in the library table
+ int index1 = mLibraryTable.getSelectionIndex();
+ // get the index in the library allocation table
+ int index2 = mLibraryAllocationTable.getSelectionIndex();
+ // get the MallocInfo object
+ LibraryAllocations liballoc = mLibraryAllocations.get(index1);
+ NativeAllocationInfo info = liballoc.getAllocation(index2);
+ fillDetailTable(info);
+ }
+ });
+
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(sash, 0);
+ mLibraryTable.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) {
+ sashData.left = new FormAttachment(0,
+ prefs.getInt(PREFS_LIBRARY_SASH));
+ } else {
+ sashData.left = new FormAttachment(50, 0);
+ }
+ sashData.bottom = new FormAttachment(100, 0);
+ sashData.top = new FormAttachment(0, 0); // 50% across
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(sash, 0);
+ data.right = new FormAttachment(100, 0);
+ mLibraryAllocationTable.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = top.getClientArea();
+ int right = panelRect.width - sashRect.width - minPanelWidth;
+ e.x = Math.max(Math.min(e.x, right), minPanelWidth);
+ if (e.x != sashRect.x) {
+ sashData.left = new FormAttachment(0, e.x);
+ prefs.setValue(PREFS_LIBRARY_SASH, e.y);
+ top.layout();
+ }
+ }
+ });
+ }
+
+ private void emptyTables() {
+ mAllocationTable.removeAll();
+ mLibraryTable.removeAll();
+ mLibraryAllocationTable.removeAll();
+ mDetailTable.removeAll();
+ }
+
+ private void sortAllocationsPerLibrary() {
+ if (mClientData != null) {
+ mLibraryAllocations.clear();
+
+ // create a hash map of LibraryAllocations to access aggregate
+ // objects already created
+ HashMap<String, LibraryAllocations> libcache =
+ new HashMap<String, LibraryAllocations>();
+
+ // get the allocation count
+ int count = mDisplayedAllocations.size();
+ for (int i = 0; i < count; i++) {
+ NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i);
+
+ NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo();
+ if (stackCallInfo != null) {
+ String libraryName = stackCallInfo.getLibraryName();
+ LibraryAllocations liballoc = libcache.get(libraryName);
+ if (liballoc == null) {
+ // didn't find a library allocation object already
+ // created so we create one
+ liballoc = new LibraryAllocations(libraryName);
+ // add it to the cache
+ libcache.put(libraryName, liballoc);
+ // add it to the list
+ mLibraryAllocations.add(liballoc);
+ }
+ // add the MallocInfo object to it.
+ liballoc.addAllocation(allocInfo);
+ }
+ }
+ // now that the list is created, we need to compute the size and
+ // sort it by size. This will also sort the MallocInfo objects
+ // inside each LibraryAllocation objects.
+ for (LibraryAllocations liballoc : mLibraryAllocations) {
+ liballoc.computeAllocationSizeAndCount();
+ }
+
+ // now we sort it
+ Collections.sort(mLibraryAllocations,
+ new Comparator<LibraryAllocations>() {
+ public int compare(LibraryAllocations o1,
+ LibraryAllocations o2) {
+ return o2.getSize() - o1.getSize();
+ }
+ });
+ }
+ }
+
+ private void renderBitmap(ClientData cd) {
+ byte[] pixData;
+
+ // Atomically get and clear the heap data.
+ synchronized (cd) {
+ if (serializeHeapData(cd.getVmHeapData()) == false) {
+ // no change, we return.
+ return;
+ }
+
+ pixData = getSerializedData();
+
+ ImageData id = createLinearHeapImage(pixData, 200, mMapPalette);
+ Image image = new Image(mBase.getDisplay(), id);
+ mImage.setImage(image);
+ mImage.pack(true);
+ }
+ }
+
+ /*
+ * Create color palette for map. Set up titles for legend.
+ */
+ private static PaletteData createPalette() {
+ RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+ colors[0]
+ = new RGB(192, 192, 192); // non-heap pixels are gray
+ mMapLegend[0]
+ = "(heap expansion area)";
+
+ colors[1]
+ = new RGB(0, 0, 0); // free chunks are black
+ mMapLegend[1]
+ = "free";
+
+ colors[HeapSegmentElement.KIND_OBJECT + 2]
+ = new RGB(0, 0, 255); // objects are blue
+ mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+ = "data object";
+
+ colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = new RGB(0, 255, 0); // class objects are green
+ mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+ = "class object";
+
+ colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = new RGB(255, 0, 0); // byte/bool arrays are red
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+ = "1-byte array (byte[], boolean[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = new RGB(255, 128, 0); // short/char arrays are orange
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+ = "2-byte array (short[], char[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = new RGB(255, 255, 0); // obj/int/float arrays are yellow
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+ = "4-byte array (object[], int[], float[])";
+
+ colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = new RGB(255, 128, 128); // long/double arrays are pink
+ mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+ = "8-byte array (long[], double[])";
+
+ colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = new RGB(255, 0, 255); // unknown objects are cyan
+ mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+ = "unknown object";
+
+ colors[HeapSegmentElement.KIND_NATIVE + 2]
+ = new RGB(64, 64, 64); // native objects are dark gray
+ mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+ = "non-Java object";
+
+ return new PaletteData(colors);
+ }
+
+ private void saveAllocations(String fileName) {
+ try {
+ PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
+
+ for (NativeAllocationInfo alloc : mAllocations) {
+ out.println(alloc.toString());
+ }
+ out.close();
+ } catch (IOException e) {
+ Log.e("Native", e);
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java
new file mode 100644
index 0000000..d910cc7
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+
+/**
+ * Base class for our information panels.
+ */
+public abstract class Panel {
+
+ public final Control createPanel(Composite parent) {
+ Control panelControl = createControl(parent);
+
+ postCreation();
+
+ return panelControl;
+ }
+
+ protected abstract void postCreation();
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ protected abstract Control createControl(Composite parent);
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ public abstract void setFocus();
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java
new file mode 100644
index 0000000..533372e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Edit an integer field, validating it as a port number.
+ */
+public class PortFieldEditor extends IntegerFieldEditor {
+
+ public boolean mRecursiveCheck = false;
+
+ public PortFieldEditor(String name, String label, Composite parent) {
+ super(name, label, parent);
+ setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+ }
+
+ /*
+ * Get the current value of the field, as an integer.
+ */
+ public int getCurrentValue() {
+ int val;
+ try {
+ val = Integer.parseInt(getStringValue());
+ }
+ catch (NumberFormatException nfe) {
+ val = -1;
+ }
+ return val;
+ }
+
+ /*
+ * Check the validity of the field.
+ */
+ @Override
+ protected boolean checkState() {
+ if (super.checkState() == false) {
+ return false;
+ }
+ //Log.i("ddms", "check state " + getStringValue());
+ boolean err = false;
+ int val = getCurrentValue();
+ if (val < 1024 || val > 32767) {
+ setErrorMessage("Port must be between 1024 and 32767");
+ err = true;
+ } else {
+ setErrorMessage(null);
+ err = false;
+ }
+ showErrorMessage();
+ return !err;
+ }
+
+ protected void updateCheckState(PortFieldEditor pfe) {
+ pfe.refreshValidState();
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java
new file mode 100644
index 0000000..dad54dc
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.RawImage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.IOException;
+
+
+/**
+ * Gather a screen shot from the device and save it to a file.
+ */
+public class ScreenShotDialog extends Dialog {
+
+ private Label mBusyLabel;
+ private Label mImageLabel;
+ private Button mSave;
+ private Device mDevice;
+
+
+ /**
+ * Create with default style.
+ */
+ public ScreenShotDialog(Shell parent) {
+ this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Create with app-defined style.
+ */
+ public ScreenShotDialog(Shell parent, int style) {
+ super(parent, style);
+ }
+
+ /**
+ * Prepare and display the dialog.
+ * @param device The {@link Device} from which to get the screenshot.
+ */
+ public void open(Device device) {
+ mDevice = device;
+
+ Shell parent = getParent();
+ Shell shell = new Shell(parent, getStyle());
+ shell.setText("Device Screen Capture");
+
+ createContents(shell);
+ shell.pack();
+ shell.open();
+
+ updateDeviceImage(shell);
+
+ Display display = parent.getDisplay();
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ }
+
+ /*
+ * Create the screen capture dialog contents.
+ */
+ private void createContents(final Shell shell) {
+ GridData data;
+
+ shell.setLayout(new GridLayout(3, true));
+
+ // title/"capturing" label
+ mBusyLabel = new Label(shell, SWT.NONE);
+ mBusyLabel.setText("Preparing...");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+ data.horizontalSpan = 3;
+ mBusyLabel.setLayoutData(data);
+
+ // space for the image
+ mImageLabel = new Label(shell, SWT.BORDER);
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.horizontalSpan = 3;
+ mImageLabel.setLayoutData(data);
+ Display display = shell.getDisplay();
+ mImageLabel.setImage(ImageHelper.createPlaceHolderArt(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)));
+
+ // "refresh" button
+ Button refresh = new Button(shell, SWT.PUSH);
+ refresh.setText("Refresh");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ refresh.setLayoutData(data);
+ refresh.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateDeviceImage(shell);
+ }
+ });
+
+ // "save" button
+ mSave = new Button(shell, SWT.PUSH);
+ mSave.setText("Save");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ mSave.setLayoutData(data);
+ mSave.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ saveImage(shell);
+ }
+ });
+
+ // "done" button
+ Button done = new Button(shell, SWT.PUSH);
+ done.setText("Done");
+ data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+ data.widthHint = 80;
+ done.setLayoutData(data);
+ done.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ shell.close();
+ }
+ });
+
+ shell.setDefaultButton(done);
+ }
+
+ /*
+ * Capture a new image from the device.
+ */
+ private void updateDeviceImage(Shell shell) {
+ mBusyLabel.setText("Capturing..."); // no effect
+
+ shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
+
+ Image image = getDeviceImage();
+ if (image == null) {
+ Display display = shell.getDisplay();
+ image = ImageHelper.createPlaceHolderArt(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE));
+ mSave.setEnabled(false);
+ mBusyLabel.setText("Screen not available");
+ } else {
+ mSave.setEnabled(true);
+ mBusyLabel.setText("Captured image:");
+ }
+
+ mImageLabel.setImage(image);
+ mImageLabel.pack();
+ shell.pack();
+
+ // there's no way to restore old cursor; assume it's ARROW
+ shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
+ }
+
+ /*
+ * Grab an image from an ADB-connected device.
+ */
+ private Image getDeviceImage() {
+ RawImage rawImage;
+
+ try {
+ rawImage = mDevice.getScreenshot();
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage());
+ return null;
+ }
+
+ // device/adb not available?
+ if (rawImage == null)
+ return null;
+
+ // convert raw data to an Image
+ assert rawImage.bpp == 16;
+ PaletteData palette = new PaletteData(0xf800, 0x07e0, 0x001f);
+ ImageData imageData = new ImageData(rawImage.width, rawImage.height,
+ rawImage.bpp, palette, 1, rawImage.data);
+
+ return new Image(getParent().getDisplay(), imageData);
+ }
+
+ /*
+ * Prompt the user to save the image to disk.
+ */
+ private void saveImage(Shell shell) {
+ FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+ String fileName;
+
+ dlg.setText("Save image...");
+ dlg.setFileName("device.png");
+ dlg.setFilterPath(DdmUiPreferences.getStore().getString("lastImageSaveDir"));
+ dlg.setFilterNames(new String[] {
+ "PNG Files (*.png)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.png" //$NON-NLS-1$
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ DdmUiPreferences.getStore().setValue("lastImageSaveDir", dlg.getFilterPath());
+
+ Log.i("ddms", "Saving image to " + fileName);
+ ImageData imageData = mImageLabel.getImage().getImageData();
+
+ try {
+ WritePng.savePng(fileName, imageData);
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "Unable to save " + fileName + ": " + ioe);
+ }
+
+ if (false) {
+ ImageLoader loader = new ImageLoader();
+ loader.data = new ImageData[] { imageData };
+ // PNG writing not available until 3.3? See bug at:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=24697
+ // GIF writing only works for 8 bits
+ // JPEG uses lossy compression
+ // BMP has screwed-up colors
+ loader.save(fileName, SWT.IMAGE_JPEG);
+ }
+ }
+ }
+
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java
new file mode 100644
index 0000000..4b5fe56
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Device;
+
+/**
+ * A Panel that requires {@link Device}/{@link Client} selection notifications.
+ */
+public abstract class SelectionDependentPanel extends Panel {
+ private Device mCurrentDevice = null;
+ private Client mCurrentClient = null;
+
+ /**
+ * Returns the current {@link Device}.
+ * @return the current device or null if none are selected.
+ */
+ protected final Device getCurrentDevice() {
+ return mCurrentDevice;
+ }
+
+ /**
+ * Returns the current {@link Client}.
+ * @return the current client or null if none are selected.
+ */
+ protected final Client getCurrentClient() {
+ return mCurrentClient;
+ }
+
+ /**
+ * Sent when a new device is selected.
+ * @param selectedDevice the selected device.
+ */
+ public final void deviceSelected(Device selectedDevice) {
+ if (selectedDevice != mCurrentDevice) {
+ mCurrentDevice = selectedDevice;
+ deviceSelected();
+ }
+ }
+
+ /**
+ * Sent when a new client is selected.
+ * @param selectedClient the selected client.
+ */
+ public final void clientSelected(Client selectedClient) {
+ if (selectedClient != mCurrentClient) {
+ mCurrentClient = selectedClient;
+ clientSelected();
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ public abstract void deviceSelected();
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ public abstract void clientSelected();
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java
new file mode 100644
index 0000000..3358962
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IStackTraceInfo;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Stack Trace Panel.
+ * <p/>This is not a panel in the regular sense. Instead this is just an object around the creation
+ * and management of a Stack Trace display.
+ * <p/>UI creation is done through
+ * {@link #createPanel(Composite, String, String, String, String, String, IPreferenceStore)}.
+ *
+ */
+public final class StackTracePanel {
+
+ private static ISourceRevealer sSourceRevealer;
+
+ private Table mStackTraceTable;
+ private TableViewer mStackTraceViewer;
+
+ private Client mCurrentClient;
+
+
+ /**
+ * Content Provider to display the stack trace of a thread.
+ * Expected input is a {@link IStackTraceInfo} object.
+ */
+ private static class StackTraceContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof IStackTraceInfo) {
+ // getElement cannot return null, so we return an empty array
+ // if there's no stack trace
+ StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace();
+ if (trace != null) {
+ return trace;
+ }
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+
+ /**
+ * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be
+ * of type {@link StackTraceElement}.
+ */
+ private static class StackTraceLabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof StackTraceElement) {
+ StackTraceElement traceElement = (StackTraceElement)element;
+ switch (columnIndex) {
+ case 0:
+ return traceElement.getClassName();
+ case 1:
+ return traceElement.getMethodName();
+ case 2:
+ return traceElement.getFileName();
+ case 3:
+ return Integer.toString(traceElement.getLineNumber());
+ case 4:
+ return Boolean.toString(traceElement.isNativeMethod());
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide a method that is able to reveal a method
+ * in a source editor
+ */
+ public interface ISourceRevealer {
+ /**
+ * Sent to reveal a particular line in a source editor
+ * @param applicationName the name of the application running the source.
+ * @param className the fully qualified class name
+ * @param line the line to reveal
+ */
+ public void reveal(String applicationName, String className, int line);
+ }
+
+
+ /**
+ * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor.
+ * @param revealer
+ */
+ public static void setSourceRevealer(ISourceRevealer revealer) {
+ sSourceRevealer = revealer;
+ }
+
+ /**
+ * Creates the controls for the StrackTrace display.
+ * <p/>This method will set the parent {@link Composite} to use a {@link GridLayout} with
+ * 2 columns.
+ * @param parent the parent composite.
+ * @param prefs_stack_col_class
+ * @param prefs_stack_col_method
+ * @param prefs_stack_col_file
+ * @param prefs_stack_col_line
+ * @param prefs_stack_col_native
+ * @param store
+ */
+ public Table createPanel(Composite parent, String prefs_stack_col_class,
+ String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line,
+ String prefs_stack_col_native, IPreferenceStore store) {
+
+ mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+ mStackTraceTable.setHeaderVisible(true);
+ mStackTraceTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Class",
+ SWT.LEFT,
+ "SomeLongClassName", //$NON-NLS-1$
+ prefs_stack_col_class, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Method",
+ SWT.LEFT,
+ "someLongMethod", //$NON-NLS-1$
+ prefs_stack_col_method, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "File",
+ SWT.LEFT,
+ "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$
+ prefs_stack_col_file, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Line",
+ SWT.RIGHT,
+ "99999", //$NON-NLS-1$
+ prefs_stack_col_line, store);
+
+ TableHelper.createTableColumn(
+ mStackTraceTable,
+ "Native",
+ SWT.LEFT,
+ "Native", //$NON-NLS-1$
+ prefs_stack_col_native, store);
+
+ mStackTraceViewer = new TableViewer(mStackTraceTable);
+ mStackTraceViewer.setContentProvider(new StackTraceContentProvider());
+ mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider());
+
+ mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ if (sSourceRevealer != null && mCurrentClient != null) {
+ // get the selected stack trace element
+ ISelection selection = mStackTraceViewer.getSelection();
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof StackTraceElement) {
+ StackTraceElement traceElement = (StackTraceElement)object;
+
+ if (traceElement.isNativeMethod() == false) {
+ sSourceRevealer.reveal(
+ mCurrentClient.getClientData().getClientDescription(),
+ traceElement.getClassName(),
+ traceElement.getLineNumber());
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return mStackTraceTable;
+ }
+
+ /**
+ * Sets the input for the {@link TableViewer}.
+ * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of
+ * {@link StackTraceElement}
+ */
+ public void setViewerInput(IStackTraceInfo input) {
+ mStackTraceViewer.setInput(input);
+ mStackTraceViewer.refresh();
+ }
+
+ /**
+ * Sets the current client running the stack trace.
+ * @param currentClient the {@link Client}.
+ */
+ public void setCurrentClient(Client currentClient) {
+ mCurrentClient = currentClient;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java
new file mode 100644
index 0000000..8ef237c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java
@@ -0,0 +1,582 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.data.general.DefaultPieDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Displays system information graphs obtained from a bugreport file or device.
+ */
+public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {
+
+ // UI components
+ private Label mLabel;
+ private Button mFetchButton;
+ private Combo mDisplayMode;
+
+ private DefaultPieDataset mDataset;
+
+ // The bugreport file to process
+ private File mDataFile;
+
+ // To get output from adb commands
+ private FileOutputStream mTempStream;
+
+ // Selects the current display: MODE_CPU, etc.
+ private int mMode = 0;
+
+ private static final int MODE_CPU = 0;
+ private static final int MODE_ALARM = 1;
+ private static final int MODE_WAKELOCK = 2;
+ private static final int MODE_MEMINFO = 3;
+ private static final int MODE_SYNC = 4;
+
+ // argument to dumpsys; section in the bugreport holding the data
+ private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm",
+ "batteryinfo", "MEMORY INFO", "content"};
+
+ private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
+ "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
+ "dumpsys content"};
+
+ private static final String CAPTIONS[] = {"CPU load", "Alarms",
+ "Wakelocks", "Memory usage", "Sync"};
+
+ /**
+ * Generates the dataset to display.
+ *
+ * @param file The bugreport file to process.
+ */
+ public void generateDataset(File file) {
+ mDataset.clear();
+ mLabel.setText("");
+ if (file == null) {
+ return;
+ }
+ try {
+ BufferedReader br = getBugreportReader(file);
+ if (mMode == MODE_CPU) {
+ readCpuDataset(br);
+ } else if (mMode == MODE_ALARM) {
+ readAlarmDataset(br);
+ } else if (mMode == MODE_WAKELOCK) {
+ readWakelockDataset(br);
+ } else if (mMode == MODE_MEMINFO) {
+ readMeminfoDataset(br);
+ } else if (mMode == MODE_SYNC) {
+ readSyncDataset(br);
+ }
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed with
+ * {@link #getCurrentDevice()}
+ */
+ @Override
+ public void deviceSelected() {
+ if (getCurrentDevice() != null) {
+ mFetchButton.setEnabled(true);
+ loadFromDevice();
+ } else {
+ mFetchButton.setEnabled(false);
+ }
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed with
+ * {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mDisplayMode.setFocus();
+ }
+
+ /**
+ * Fetches a new bugreport from the device and updates the display.
+ * Fetching is asynchronous. See also addOutput, flush, and isCancelled.
+ */
+ private void loadFromDevice() {
+ try {
+ initShellOutputBuffer();
+ if (mMode == MODE_MEMINFO) {
+ // Hack to add bugreport-style section header for meminfo
+ mTempStream.write("------ MEMORY INFO ------\n".getBytes());
+ }
+ getCurrentDevice().executeShellCommand(
+ DUMP_COMMAND[mMode], this);
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Initializes temporary output file for executeShellCommand().
+ *
+ * @throws IOException on file error
+ */
+ void initShellOutputBuffer() throws IOException {
+ mDataFile = File.createTempFile("ddmsfile", ".txt");
+ mDataFile.deleteOnExit();
+ mTempStream = new FileOutputStream(mDataFile);
+ }
+
+ /**
+ * Adds output to the temp file. IShellOutputReceiver method. Called by
+ * executeShellCommand().
+ */
+ public void addOutput(byte[] data, int offset, int length) {
+ try {
+ mTempStream.write(data, offset, length);
+ }
+ catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+
+ /**
+ * Processes output from shell command. IShellOutputReceiver method. The
+ * output is passed to generateDataset(). Called by executeShellCommand() on
+ * completion.
+ */
+ public void flush() {
+ if (mTempStream != null) {
+ try {
+ mTempStream.close();
+ generateDataset(mDataFile);
+ mTempStream = null;
+ mDataFile = null;
+ } catch (IOException e) {
+ Log.e("DDMS", e);
+ }
+ }
+ }
+
+ /**
+ * IShellOutputReceiver method.
+ *
+ * @return false - don't cancel
+ */
+ public boolean isCancelled() {
+ return false;
+ }
+
+ /**
+ * Create our controls for the UI panel.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, false));
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Composite buttons = new Composite(top, SWT.NONE);
+ buttons.setLayout(new RowLayout());
+
+ mDisplayMode = new Combo(buttons, SWT.PUSH);
+ for (String mode : CAPTIONS) {
+ mDisplayMode.add(mode);
+ }
+ mDisplayMode.select(mMode);
+ mDisplayMode.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mMode = mDisplayMode.getSelectionIndex();
+ if (mDataFile != null) {
+ generateDataset(mDataFile);
+ } else if (getCurrentDevice() != null) {
+ loadFromDevice();
+ }
+ }
+ });
+
+ final Button loadButton = new Button(buttons, SWT.PUSH);
+ loadButton.setText("Load from File");
+ loadButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(loadButton.getShell(),
+ SWT.OPEN);
+ fileDialog.setText("Load bugreport");
+ String filename = fileDialog.open();
+ if (filename != null) {
+ mDataFile = new File(filename);
+ generateDataset(mDataFile);
+ }
+ }
+ });
+
+ mFetchButton = new Button(buttons, SWT.PUSH);
+ mFetchButton.setText("Update from Device");
+ mFetchButton.setEnabled(false);
+ mFetchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ loadFromDevice();
+ }
+ });
+
+ mLabel = new Label(top, SWT.NONE);
+ mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mDataset = new DefaultPieDataset();
+ JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
+ /* legend */, true/* tooltips */, false /* urls */);
+
+ ChartComposite chartComposite = new ChartComposite(top,
+ SWT.BORDER, chart,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000,
+ // max draw width. We don't want it to zoom, so we put a big number
+ 3000,
+ // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ false, // zoom
+ true);
+ chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ return top;
+ }
+
+ public void clientChanged(final Client client, int changeMask) {
+ // Don't care
+ }
+
+ /**
+ * Helper to open a bugreport and skip to the specified section.
+ *
+ * @param file File to open
+ * @return Reader to bugreport file
+ * @throws java.io.IOException on file error
+ */
+ private BufferedReader getBugreportReader(File file) throws
+ IOException {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ // Skip over the unwanted bugreport sections
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ Log.d("DDMS", "Service not found " + line);
+ break;
+ }
+ if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
+ line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
+ break;
+ }
+ }
+ return br;
+ }
+
+ /**
+ * Parse the time string generated by BatteryStats.
+ * A typical new-format string is "11d 13h 45m 39s 999ms".
+ * A typical old-format string is "12.3 sec".
+ * @return time in ms
+ */
+ private static long parseTimeMs(String s) {
+ long total = 0;
+ // Matches a single component e.g. "12.3 sec" or "45ms"
+ Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
+ Matcher m = p.matcher(s);
+ while (m.find()) {
+ String label = m.group(2);
+ if ("sec".equals(label)) {
+ // Backwards compatibility with old time format
+ total += (long) (Double.parseDouble(m.group(1)) * 1000);
+ continue;
+ }
+ long value = Integer.parseInt(m.group(1));
+ if ("d".equals(label)) {
+ total += value * 24 * 60 * 60 * 1000;
+ } else if ("h".equals(label)) {
+ total += value * 60 * 60 * 1000;
+ } else if ("m".equals(label)) {
+ total += value * 60 * 1000;
+ } else if ("s".equals(label)) {
+ total += value * 1000;
+ } else if ("ms".equals(label)) {
+ total += value;
+ }
+ }
+ return total;
+ }
+ /**
+ * Processes wakelock information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readWakelockDataset(BufferedReader br) throws IOException {
+ Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
+ Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
+ double total = 0;
+ boolean inCurrent = false;
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith("Current Battery Usage Statistics")) {
+ inCurrent = true;
+ } else if (inCurrent) {
+ Matcher m = lockPattern.matcher(line);
+ if (m.find()) {
+ double value = parseTimeMs(m.group(2)) / 1000.;
+ mDataset.setValue(m.group(1), value);
+ total -= value;
+ } else {
+ m = totalPattern.matcher(line);
+ if (m.find()) {
+ total += parseTimeMs(m.group(1)) / 1000.;
+ }
+ }
+ }
+ }
+ if (total > 0) {
+ mDataset.setValue("Unlocked", total);
+ }
+ }
+
+ /**
+ * Processes alarm information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readAlarmDataset(BufferedReader br) throws IOException {
+ Pattern pattern = Pattern
+ .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ long count = Long.parseLong(m.group(1));
+ String name = m.group(2);
+ mDataset.setValue(name, count);
+ }
+ }
+ }
+
+ /**
+ * Processes cpu load information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readCpuDataset(BufferedReader br) throws IOException {
+ Pattern pattern = Pattern
+ .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
+
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith("Load:")) {
+ mLabel.setText(line);
+ continue;
+ }
+ Matcher m = pattern.matcher(line);
+ if (m.find()) {
+ String name = m.group(1);
+ long both = Long.parseLong(m.group(2));
+ long user = Long.parseLong(m.group(3));
+ long kernel = Long.parseLong(m.group(4));
+ if ("TOTAL".equals(name)) {
+ if (both < 100) {
+ mDataset.setValue("Idle", (100 - both));
+ }
+ } else {
+ // Try to make graphs more useful even with rounding;
+ // log often has 0% user + 0% kernel = 1% total
+ // We arbitrarily give extra to kernel
+ if (user > 0) {
+ mDataset.setValue(name + " (user)", user);
+ }
+ if (kernel > 0) {
+ mDataset.setValue(name + " (kernel)" , both - user);
+ }
+ if (user == 0 && kernel == 0 && both > 0) {
+ mDataset.setValue(name, both);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes meminfo information from bugreport. Updates mDataset with the
+ * new data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readMeminfoDataset(BufferedReader br) throws IOException {
+ Pattern valuePattern = Pattern.compile("(\\d+) kB");
+ long total = 0;
+ long other = 0;
+ mLabel.setText("PSS in kB");
+
+ // Scan meminfo
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ // End of file
+ break;
+ }
+ Matcher m = valuePattern.matcher(line);
+ if (m.find()) {
+ long kb = Long.parseLong(m.group(1));
+ if (line.startsWith("MemTotal")) {
+ total = kb;
+ } else if (line.startsWith("MemFree")) {
+ mDataset.setValue("Free", kb);
+ total -= kb;
+ } else if (line.startsWith("Slab")) {
+ mDataset.setValue("Slab", kb);
+ total -= kb;
+ } else if (line.startsWith("PageTables")) {
+ mDataset.setValue("PageTables", kb);
+ total -= kb;
+ } else if (line.startsWith("Buffers") && kb > 0) {
+ mDataset.setValue("Buffers", kb);
+ total -= kb;
+ } else if (line.startsWith("Inactive")) {
+ mDataset.setValue("Inactive", kb);
+ total -= kb;
+ } else if (line.startsWith("MemFree")) {
+ mDataset.setValue("Free", kb);
+ total -= kb;
+ }
+ } else {
+ break;
+ }
+ }
+ // Scan procrank
+ while (true) {
+ String line = br.readLine();
+ if (line == null) {
+ break;
+ }
+ if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
+ // procrank header
+ continue;
+ }
+ if (line.indexOf("----") >= 0) {
+ //end of procrank section
+ break;
+ }
+ // Extract pss field from procrank output
+ long pss = Long.parseLong(line.substring(23, 31).trim());
+ String cmdline = line.substring(43).trim().replace("/system/bin/", "");
+ // Arbitrary minimum size to display
+ if (pss > 2000) {
+ mDataset.setValue(cmdline, pss);
+ } else {
+ other += pss;
+ }
+ total -= pss;
+ }
+ mDataset.setValue("Other", other);
+ mDataset.setValue("Unknown", total);
+ }
+
+ /**
+ * Processes sync information from bugreport. Updates mDataset with the new
+ * data.
+ *
+ * @param br Reader providing the content
+ * @throws IOException if error reading file
+ */
+ void readSyncDataset(BufferedReader br) throws IOException {
+ while (true) {
+ String line = br.readLine();
+ if (line == null || line.startsWith("DUMP OF SERVICE")) {
+ // Done, or moved on to the next service
+ break;
+ }
+ if (line.startsWith(" |") && line.length() > 70) {
+ String authority = line.substring(3, 18).trim();
+ String duration = line.substring(61, 70).trim();
+ // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
+ String durParts[] = duration.split(":");
+ if (durParts.length == 2) {
+ long dur = Long.parseLong(durParts[0]) * 60 + Long
+ .parseLong(durParts[1]);
+ mDataset.setValue(authority, dur);
+ } else if (duration.length() == 3) {
+ long dur = Long.parseLong(durParts[0]) * 3600
+ + Long.parseLong(durParts[1]) * 60 + Long
+ .parseLong(durParts[2]);
+ mDataset.setValue(authority, dur);
+ }
+ }
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java
new file mode 100644
index 0000000..f8d457e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Utility class to help using Table objects.
+ *
+ */
+public final class TableHelper {
+ /**
+ * Create a TableColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> object and a preference entry name String
+ * object are provided then the column will listen to change in its width
+ * and update the preference store accordingly.
+ *
+ * @param parent The Table parent object
+ * @param header The header string
+ * @param style The column style
+ * @param sample_text A sample text to figure out column width if preference
+ * value is missing
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ * @return The TableColumn object that was created
+ */
+ public static TableColumn createTableColumn(Table parent, String header,
+ int style, String sample_text, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TableColumn col = new TableColumn(parent, style);
+
+ // if there is no pref store or the entry is missing, we use the sample
+ // text and pack the column.
+ // Otherwise we just read the width from the prefs and apply it.
+ if (prefs == null || prefs.contains(pref_name) == false) {
+ col.setText(sample_text);
+ col.pack();
+
+ // init the prefs store with the current value
+ if (prefs != null) {
+ prefs.setValue(pref_name, col.getWidth());
+ }
+ } else {
+ col.setWidth(prefs.getInt(pref_name));
+ }
+
+ // set the header
+ col.setText(header);
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put store the new width value.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TableColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+
+ return col;
+ }
+
+ /**
+ * Create a TreeColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> object and a preference entry name String
+ * object are provided then the column will listen to change in its width
+ * and update the preference store accordingly.
+ *
+ * @param parent The Table parent object
+ * @param header The header string
+ * @param style The column style
+ * @param sample_text A sample text to figure out column width if preference
+ * value is missing
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ */
+ public static void createTreeColumn(Tree parent, String header, int style,
+ String sample_text, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TreeColumn col = new TreeColumn(parent, style);
+
+ // if there is no pref store or the entry is missing, we use the sample
+ // text and pack the column.
+ // Otherwise we just read the width from the prefs and apply it.
+ if (prefs == null || prefs.contains(pref_name) == false) {
+ col.setText(sample_text);
+ col.pack();
+
+ // init the prefs store with the current value
+ if (prefs != null) {
+ prefs.setValue(pref_name, col.getWidth());
+ }
+ } else {
+ col.setWidth(prefs.getInt(pref_name));
+ }
+
+ // set the header
+ col.setText(header);
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put store the new width value.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TreeColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java
new file mode 100644
index 0000000..b037193
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.Arrays;
+
+/**
+ * Base class for panel containing Table that need to support copy-paste-selectAll
+ */
+public abstract class TablePanel extends ClientDisplayPanel {
+ private ITableFocusListener mGlobalListener;
+
+ /**
+ * Sets a TableFocusListener which will be notified when one of the tables
+ * gets or loses focus.
+ *
+ * @param listener
+ */
+ public final void setTableFocusListener(ITableFocusListener listener) {
+ // record the global listener, to make sure table created after
+ // this call will still be setup.
+ mGlobalListener = listener;
+
+ setTableFocusListener();
+ }
+
+ /**
+ * Sets up the Table of object of the panel to work with the global listener.<br>
+ * Default implementation does nothing.
+ */
+ protected void setTableFocusListener() {
+
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.
+ *
+ * @param table the Table object.
+ * @param colStart
+ * @param colEnd
+ */
+ protected final void addTableToFocusListener(final Table table,
+ final int colStart, final int colEnd) {
+ // create the activator for this table
+ final IFocusedTableActivator activator = new IFocusedTableActivator() {
+ public void copy(Clipboard clipboard) {
+ int[] selection = table.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // all lines must be concatenated.
+ StringBuilder sb = new StringBuilder();
+
+ // loop on the selection and output the file.
+ for (int i : selection) {
+ TableItem item = table.getItem(i);
+ for (int c = colStart ; c <= colEnd ; c++) {
+ sb.append(item.getText(c));
+ sb.append('\t');
+ }
+ sb.append('\n');
+ }
+
+ // now add that to the clipboard if the string has content
+ String data = sb.toString();
+ if (data != null || data.length() > 0) {
+ clipboard.setContents(
+ new Object[] { data },
+ new Transfer[] { TextTransfer.getInstance() });
+ }
+ }
+
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ public void focusLost(FocusEvent e) {
+ mGlobalListener.focusLost(activator);
+ }
+ });
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.<br>
+ * When the copy method is invoked, all columns are put in the clipboard, separated
+ * by tabs
+ *
+ * @param table the Table object.
+ */
+ protected final void addTableToFocusListener(final Table table) {
+ addTableToFocusListener(table, 0, table.getColumnCount()-1);
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java
new file mode 100644
index 0000000..a034063
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ThreadInfo;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Base class for our information panels.
+ */
+public class ThreadPanel extends TablePanel {
+
+ private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$
+ private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$
+
+ private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$
+
+ private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$
+ private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$
+
+ private Display mDisplay;
+ private Composite mBase;
+ private Label mNotEnabled;
+ private Label mNotSelected;
+
+ private Composite mThreadBase;
+ private Table mThreadTable;
+ private TableViewer mThreadViewer;
+
+ private Composite mStackTraceBase;
+ private Button mRefreshStackTraceButton;
+ private Label mStackTraceTimeLabel;
+ private StackTracePanel mStackTracePanel;
+ private Table mStackTraceTable;
+
+ /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */
+ private boolean mMustStopRecurringThreadUpdate = false;
+ /** Flag to tell the recurring thread update to stop running */
+ private boolean mRecurringThreadUpdateRunning = false;
+
+ private Object mLock = new Object();
+
+ private static final String[] THREAD_STATUS = {
+ "zombie", "running", "timed-wait", "monitor",
+ "wait", "init", "start", "native", "vmwait"
+ };
+
+ /**
+ * Content Provider to display the threads of a client.
+ * Expected input is a {@link Client} object.
+ */
+ private static class ThreadContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Client) {
+ return ((Client)inputElement).getClientData().getThreads();
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+
+ /**
+ * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be
+ * of type {@link ThreadInfo}.
+ */
+ private static class ThreadLabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof ThreadInfo) {
+ ThreadInfo thread = (ThreadInfo)element;
+ switch (columnIndex) {
+ case 0:
+ return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$
+ String.valueOf(thread.getThreadId());
+ case 1:
+ return String.valueOf(thread.getTid());
+ case 2:
+ if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length)
+ return THREAD_STATUS[thread.getStatus()];
+ return "unknown";
+ case 3:
+ return String.valueOf(thread.getUtime());
+ case 4:
+ return String.valueOf(thread.getStime());
+ case 5:
+ return thread.getThreadName();
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Create our control(s).
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mDisplay = parent.getDisplay();
+
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ mBase = new Composite(parent, SWT.NONE);
+ mBase.setLayout(new StackLayout());
+
+ // UI for thread not enabled
+ mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP);
+ mNotEnabled.setText("Thread updates not enabled for selected client\n"
+ + "(use toolbar button to enable)");
+
+ // UI for not client selected
+ mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP);
+ mNotSelected.setText("no client is selected");
+
+ // base composite for selected client with enabled thread update.
+ mThreadBase = new Composite(mBase, SWT.NONE);
+ mThreadBase.setLayout(new FormLayout());
+
+ // table above the sash
+ mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION);
+ mThreadTable.setHeaderVisible(true);
+ mThreadTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "ID",
+ SWT.RIGHT,
+ "888", //$NON-NLS-1$
+ PREFS_THREAD_COL_ID, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Tid",
+ SWT.RIGHT,
+ "88888", //$NON-NLS-1$
+ PREFS_THREAD_COL_TID, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Status",
+ SWT.LEFT,
+ "timed-wait", //$NON-NLS-1$
+ PREFS_THREAD_COL_STATUS, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "utime",
+ SWT.RIGHT,
+ "utime", //$NON-NLS-1$
+ PREFS_THREAD_COL_UTIME, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "stime",
+ SWT.RIGHT,
+ "utime", //$NON-NLS-1$
+ PREFS_THREAD_COL_STIME, store);
+
+ TableHelper.createTableColumn(
+ mThreadTable,
+ "Name",
+ SWT.LEFT,
+ "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$
+ PREFS_THREAD_COL_NAME, store);
+
+ mThreadViewer = new TableViewer(mThreadTable);
+ mThreadViewer.setContentProvider(new ThreadContentProvider());
+ mThreadViewer.setLabelProvider(new ThreadLabelProvider());
+
+ mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ updateThreadStackTrace(selectedThread);
+ }
+ });
+ mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ ThreadInfo selectedThread = getThreadSelection(event.getSelection());
+ if (selectedThread != null) {
+ Client client = (Client)mThreadViewer.getInput();
+
+ if (client != null) {
+ client.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ // the separating sash
+ final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL);
+ Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+ sash.setBackground(darkGray);
+
+ // the UI below the sash
+ mStackTraceBase = new Composite(mThreadBase, SWT.NONE);
+ mStackTraceBase.setLayout(new GridLayout(2, false));
+
+ mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH);
+ mRefreshStackTraceButton.setText("Refresh");
+ mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ ThreadInfo selectedThread = getThreadSelection(null);
+ if (selectedThread != null) {
+ Client currentClient = getCurrentClient();
+ if (currentClient != null) {
+ currentClient.requestThreadStackTrace(selectedThread.getThreadId());
+ }
+ }
+ }
+ });
+
+ mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
+ mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mStackTracePanel = new StackTracePanel();
+ mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase,
+ PREFS_STACK_COL_CLASS,
+ PREFS_STACK_COL_METHOD,
+ PREFS_STACK_COL_FILE,
+ PREFS_STACK_COL_LINE,
+ PREFS_STACK_COL_NATIVE,
+ store);
+
+ GridData gd;
+ mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 2;
+
+ // now setup the sash.
+ // form layout data
+ FormData data = new FormData();
+ data.top = new FormAttachment(0, 0);
+ data.bottom = new FormAttachment(sash, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mThreadTable.setLayoutData(data);
+
+ final FormData sashData = new FormData();
+ if (store != null && store.contains(PREFS_THREAD_SASH)) {
+ sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH));
+ } else {
+ sashData.top = new FormAttachment(50,0); // 50% across
+ }
+ sashData.left = new FormAttachment(0, 0);
+ sashData.right = new FormAttachment(100, 0);
+ sash.setLayoutData(sashData);
+
+ data = new FormData();
+ data.top = new FormAttachment(sash, 0);
+ data.bottom = new FormAttachment(100, 0);
+ data.left = new FormAttachment(0, 0);
+ data.right = new FormAttachment(100, 0);
+ mStackTraceBase.setLayoutData(data);
+
+ // allow resizes, but cap at minPanelWidth
+ sash.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event e) {
+ Rectangle sashRect = sash.getBounds();
+ Rectangle panelRect = mThreadBase.getClientArea();
+ int bottom = panelRect.height - sashRect.height - 100;
+ e.y = Math.max(Math.min(e.y, bottom), 100);
+ if (e.y != sashRect.y) {
+ sashData.top = new FormAttachment(0, e.y);
+ store.setValue(PREFS_THREAD_SASH, e.y);
+ mThreadBase.layout();
+ }
+ }
+ });
+
+ ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+
+ return mBase;
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mThreadTable.setFocus();
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ if (client == getCurrentClient()) {
+ if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 ||
+ (changeMask & Client.CHANGE_THREAD_DATA) != 0) {
+ try {
+ mThreadTable.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ clientSelected();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) {
+ try {
+ mThreadTable.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ updateThreadStackCall();
+ }
+ });
+ } catch (SWTException e) {
+ // widget is disposed, we do nothing
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ // pass
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ if (mThreadTable.isDisposed()) {
+ return;
+ }
+
+ Client client = getCurrentClient();
+
+ mStackTracePanel.setCurrentClient(client);
+
+ if (client != null) {
+ if (!client.isThreadUpdateEnabled()) {
+ ((StackLayout)mBase.getLayout()).topControl = mNotEnabled;
+ mThreadViewer.setInput(null);
+
+ // if we are currently updating the thread, stop doing it.
+ mMustStopRecurringThreadUpdate = true;
+ } else {
+ ((StackLayout)mBase.getLayout()).topControl = mThreadBase;
+ mThreadViewer.setInput(client);
+
+ synchronized (mLock) {
+ // if we're not updating we start the process
+ if (mRecurringThreadUpdateRunning == false) {
+ startRecurringThreadUpdate();
+ } else if (mMustStopRecurringThreadUpdate) {
+ // else if there's a runnable that's still going to get called, lets
+ // simply cancel the stop, and keep going
+ mMustStopRecurringThreadUpdate = false;
+ }
+ }
+ }
+ } else {
+ ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+ mThreadViewer.setInput(null);
+ }
+
+ mBase.layout();
+ }
+
+ /**
+ * Updates the stack call of the currently selected thread.
+ * <p/>
+ * This <b>must</b> be called from the UI thread.
+ */
+ private void updateThreadStackCall() {
+ Client client = getCurrentClient();
+ if (client != null) {
+ // get the current selection in the ThreadTable
+ ThreadInfo selectedThread = getThreadSelection(null);
+
+ if (selectedThread != null) {
+ updateThreadStackTrace(selectedThread);
+ } else {
+ updateThreadStackTrace(null);
+ }
+ }
+ }
+
+ /**
+ * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied
+ * of current data.
+ * @param thread
+ */
+ private void updateThreadStackTrace(ThreadInfo thread) {
+ mStackTracePanel.setViewerInput(thread);
+
+ if (thread != null) {
+ mRefreshStackTraceButton.setEnabled(true);
+ long stackcallTime = thread.getStackCallTime();
+ if (stackcallTime != 0) {
+ String label = new Date(stackcallTime).toString();
+ mStackTraceTimeLabel.setText(label);
+ } else {
+ mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ mRefreshStackTraceButton.setEnabled(true);
+ mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ protected void setTableFocusListener() {
+ addTableToFocusListener(mThreadTable);
+ addTableToFocusListener(mStackTraceTable);
+ }
+
+ /**
+ * Initiate recurring events. We use a shorter "initialWait" so we do the
+ * first execution sooner. We don't do it immediately because we want to
+ * give the clients a chance to get set up.
+ */
+ private void startRecurringThreadUpdate() {
+ mRecurringThreadUpdateRunning = true;
+ int initialWait = 1000;
+
+ mDisplay.timerExec(initialWait, new Runnable() {
+ public void run() {
+ synchronized (mLock) {
+ // lets check we still want updates.
+ if (mMustStopRecurringThreadUpdate == false) {
+ Client client = getCurrentClient();
+ if (client != null) {
+ client.requestThreadUpdate();
+
+ mDisplay.timerExec(
+ DdmUiPreferences.getThreadRefreshInterval() * 1000, this);
+ } else {
+ // we don't have a Client, which means the runnable is not
+ // going to be called through the timer. We reset the running flag.
+ mRecurringThreadUpdateRunning = false;
+ }
+ } else {
+ // else actually stops (don't call the timerExec) and reset the flags.
+ mRecurringThreadUpdateRunning = false;
+ mMustStopRecurringThreadUpdate = false;
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the current thread selection or <code>null</code> if none is found.
+ * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection
+ * is returned, otherwise, the <code>ISelection</code> returned by
+ * {@link TableViewer#getSelection()} is used.
+ * @param selection the {@link ISelection} to use, or <code>null</code>
+ */
+ private ThreadInfo getThreadSelection(ISelection selection) {
+ if (selection == null) {
+ selection = mThreadViewer.getSelection();
+ }
+
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof ThreadInfo) {
+ return (ThreadInfo)object;
+ }
+ }
+
+ return null;
+ }
+
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java
new file mode 100644
index 0000000..f65dafe
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.swt.graphics.ImageData;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+/**
+ * Compensate for SWT issues by writing our own PNGs from ImageData.
+ */
+public class WritePng {
+ private WritePng() {}
+
+ private static final byte[] PNG_MAGIC =
+ new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 };
+
+ public static void savePng(String fileName, ImageData imageData)
+ throws IOException {
+
+ try {
+ FileOutputStream out = new FileOutputStream(fileName);
+
+ Log.d("ddms", "Saving to PNG, width=" + imageData.width
+ + ", height=" + imageData.height
+ + ", depth=" + imageData.depth
+ + ", bpl=" + imageData.bytesPerLine);
+
+ savePng(out, imageData);
+
+ // need to do that on, or the file is empty on windows!
+ out.flush();
+ out.close();
+ } catch (Exception e) {
+ Log.e("writepng", e);
+ }
+ }
+
+ /*
+ * Supply functionality missing from our version of SWT.
+ */
+ private static void savePng(OutputStream out, ImageData imageData)
+ throws IOException {
+
+ int width = imageData.width;
+ int height = imageData.height;
+ byte[] out24;
+
+ Log.i("ddms-png", "Convert to 24bit from " + imageData.depth);
+
+ if (imageData.depth == 24 || imageData.depth == 32) {
+ out24 = convertTo24ForPng(imageData.data, width, height,
+ imageData.depth, imageData.bytesPerLine);
+ } else if (imageData.depth == 16) {
+ out24 = convert16to24(imageData);
+ } else {
+ return;
+ }
+
+ // Create the compressed form. I'm taking the low road here and
+ // just creating a large buffer, which should always be enough to
+ // hold the compressed output.
+ byte[] compPixels = new byte[out24.length + 16384];
+ Deflater compressor = new Deflater();
+ compressor.setLevel(Deflater.BEST_COMPRESSION);
+ compressor.setInput(out24);
+ compressor.finish();
+ int compLen;
+ do { // must do this in a loop to satisfy java.util.Zip
+ compLen = compressor.deflate(compPixels);
+ assert compLen != 0 || !compressor.needsInput();
+ } while (compLen == 0);
+ Log.d("ddms", "Compressed image data from " + out24.length
+ + " to " + compLen);
+
+ // Write the PNG magic
+ out.write(PNG_MAGIC);
+
+ ByteBuffer buf;
+ CRC32 crc;
+
+ // Write the IHDR chunk (13 bytes)
+ byte[] header = new byte[8 + 13 + 4];
+ buf = ByteBuffer.wrap(header);
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ putChunkHeader(buf, 13, "IHDR");
+ buf.putInt(width);
+ buf.putInt(height);
+ buf.put((byte) 8); // 8pp
+ buf.put((byte) 2); // direct color used
+ buf.put((byte) 0); // compression method == deflate
+ buf.put((byte) 0); // filter method (none)
+ buf.put((byte) 0); // interlace method (none)
+
+ crc = new CRC32();
+ crc.update(header, 4, 4+13);
+ buf.putInt((int)crc.getValue());
+
+ out.write(header);
+
+ // Write the IDAT chunk
+ byte[] datHdr = new byte[8 + 0 + 4];
+ buf = ByteBuffer.wrap(datHdr);
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ putChunkHeader(buf, compLen, "IDAT");
+ crc = new CRC32();
+ crc.update(datHdr, 4, 4+0);
+ crc.update(compPixels, 0, compLen);
+ buf.putInt((int) crc.getValue());
+
+ out.write(datHdr, 0, 8);
+ out.write(compPixels, 0, compLen);
+ out.write(datHdr, 8, 4);
+
+ // Write the IEND chunk (0 bytes)
+ byte[] trailer = new byte[8 + 0 + 4];
+
+ buf = ByteBuffer.wrap(trailer);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ putChunkHeader(buf, 0, "IEND");
+
+ crc = new CRC32();
+ crc.update(trailer, 4, 4+0);
+ buf.putInt((int)crc.getValue());
+
+ out.write(trailer);
+ }
+
+ /*
+ * Output a chunk header.
+ */
+ private static void putChunkHeader(ByteBuffer buf, int length,
+ String typeStr) {
+
+ int type = 0;
+
+ if (typeStr.length() != 4)
+ throw new RuntimeException();
+
+ for (int i = 0; i < 4; i++) {
+ type <<= 8;
+ type |= (byte) typeStr.charAt(i);
+ }
+
+ buf.putInt(length);
+ buf.putInt(type);
+ }
+
+ /*
+ * Convert raw pixels to 24-bit RGB with a "filter" byte at the start
+ * of each row.
+ */
+ private static byte[] convertTo24ForPng(byte[] in, int width, int height,
+ int depth, int stride) {
+
+ assert depth == 24 || depth == 32;
+ assert stride == width * (depth/8);
+
+ // 24 bit pixels plus one byte per line for "filter"
+ byte[] out24 = new byte[width * height * 3 + height];
+ int y;
+
+ int inOff = 0;
+ int outOff = 0;
+ for (y = 0; y < height; y++) {
+ out24[outOff++] = 0; // filter flag
+
+ if (depth == 24) {
+ System.arraycopy(in, inOff, out24, outOff, width * 3);
+ outOff += width * 3;
+ } else if (depth == 32) {
+ int tmpOff = inOff;
+ for (int x = 0; x < width; x++) {
+ tmpOff++; // ignore alpha
+ out24[outOff++] = in[tmpOff++];
+ out24[outOff++] = in[tmpOff++];
+ out24[outOff++] = in[tmpOff++];
+ }
+ }
+
+ inOff += stride;
+ }
+
+ assert outOff == out24.length;
+
+ return out24;
+ }
+
+ private static byte[] convert16to24(ImageData imageData) {
+ int width = imageData.width;
+ int height = imageData.height;
+
+ int redShift = imageData.palette.redShift;
+ int greenShift = imageData.palette.greenShift;
+ int blueShift = imageData.palette.blueShift;
+
+ int redMask = imageData.palette.redMask;
+ int greenMask = imageData.palette.greenMask;
+ int blueMask = imageData.palette.blueMask;
+
+ // 24 bit pixels plus one byte per line for "filter"
+ byte[] out24 = new byte[width * height * 3 + height];
+ int outOff = 0;
+
+
+ int[] line = new int[width];
+ for (int y = 0; y < height; y++) {
+ imageData.getPixels(0, y, width, line, 0);
+
+ out24[outOff++] = 0; // filter flag
+ for (int x = 0; x < width; x++) {
+ int pixelValue = line[x];
+ out24[outOff++] = byteChannelValue(pixelValue, redMask, redShift);
+ out24[outOff++] = byteChannelValue(pixelValue, greenMask, greenShift);
+ out24[outOff++] = byteChannelValue(pixelValue, blueMask, blueShift);
+ }
+ }
+
+ return out24;
+ }
+
+ private static byte byteChannelValue(int value, int mask, int shift) {
+ int bValue = value & mask;
+ if (shift < 0) {
+ bValue = bValue >>> -shift;
+ } else {
+ bValue = bValue << shift;
+ }
+
+ return (byte)bValue;
+
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java
new file mode 100644
index 0000000..856b874
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.actions;
+
+/**
+ * Common interface for basic action handling. This allows the common ui
+ * components to access ToolItem or Action the same way.
+ */
+public interface ICommonAction {
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ */
+ public void setEnabled(boolean enabled);
+
+ /**
+ * Sets the checked status of this action.
+ * @param checked the new checked status
+ */
+ public void setChecked(boolean checked);
+
+ /**
+ * Sets the {@link Runnable} that will be executed when the action is triggered.
+ */
+ public void setRunnable(Runnable runnable);
+}
+
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java
new file mode 100644
index 0000000..bc1598f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.actions;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+/**
+ * Wrapper around {@link ToolItem} to implement {@link ICommonAction}
+ */
+public class ToolItemAction implements ICommonAction {
+ public ToolItem item;
+
+ public ToolItemAction(ToolBar parent, int style) {
+ item = new ToolItem(parent, style);
+ }
+
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ * @see ICommonAction#setChecked(boolean)
+ */
+ public void setChecked(boolean checked) {
+ item.setSelection(checked);
+ }
+
+ /**
+ * Sets the enabled state of this action.
+ * @param enabled <code>true</code> to enable, and
+ * <code>false</code> to disable
+ * @see ICommonAction#setEnabled(boolean)
+ */
+ public void setEnabled(boolean enabled) {
+ item.setEnabled(enabled);
+ }
+
+ /**
+ * Sets the {@link Runnable} that will be executed when the action is triggered (through
+ * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}).
+ * @see ICommonAction#setRunnable(Runnable)
+ */
+ public void setRunnable(final Runnable runnable) {
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ runnable.run();
+ }
+ });
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java
new file mode 100644
index 0000000..8e9e11b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface UiThread {
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java
new file mode 100644
index 0000000..e767eda
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are not executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface WorkerThread {
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java
new file mode 100644
index 0000000..4df4376
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.console;
+
+
+/**
+ * Static Console used to ouput messages. By default outputs the message to System.out and
+ * System.err, but can receive a IDdmConsole object which will actually do something.
+ */
+public class DdmConsole {
+
+ private static IDdmConsole mConsole;
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printErrorToConsole(String message) {
+ if (mConsole != null) {
+ mConsole.printErrorToConsole(message);
+ } else {
+ System.err.println(message);
+ }
+ }
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printErrorToConsole(String[] messages) {
+ if (mConsole != null) {
+ mConsole.printErrorToConsole(messages);
+ } else {
+ for (String message : messages) {
+ System.err.println(message);
+ }
+ }
+ }
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printToConsole(String message) {
+ if (mConsole != null) {
+ mConsole.printToConsole(message);
+ } else {
+ System.out.println(message);
+ }
+ }
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ * @param forceDisplay if true, this force the console to be displayed.
+ */
+ public static void printToConsole(String[] messages) {
+ if (mConsole != null) {
+ mConsole.printToConsole(messages);
+ } else {
+ for (String message : messages) {
+ System.out.println(message);
+ }
+ }
+ }
+
+ /**
+ * Sets a IDdmConsole to override the default behavior of the console
+ * @param console The new IDdmConsole
+ * **/
+ public static void setConsole(IDdmConsole console) {
+ mConsole = console;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java
new file mode 100644
index 0000000..3679d41
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.console;
+
+
+/**
+ * DDMS console interface.
+ */
+public interface IDdmConsole {
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ */
+ public void printErrorToConsole(String message);
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ */
+ public void printErrorToConsole(String[] messages);
+
+ /**
+ * Prints a message to the android console.
+ * @param message the message to print
+ */
+ public void printToConsole(String message);
+
+ /**
+ * Prints several messages to the android console.
+ * @param messages the messages to print
+ */
+ public void printToConsole(String[] messages);
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java
new file mode 100644
index 0000000..75c19fe
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.FileListingService.IListingReceiver;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+
+/**
+ * Content provider class for device Explorer.
+ */
+class DeviceContentProvider implements ITreeContentProvider {
+
+ private TreeViewer mViewer;
+ private FileListingService mFileListingService;
+ private FileEntry mRootEntry;
+
+ private IListingReceiver sListingReceiver = new IListingReceiver() {
+ public void setChildren(final FileEntry entry, FileEntry[] children) {
+ final Tree t = mViewer.getTree();
+ if (t != null && t.isDisposed() == false) {
+ Display display = t.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ public void run() {
+ if (t.isDisposed() == false) {
+ // refresh the entry.
+ mViewer.refresh(entry);
+
+ // force it open, since on linux and windows
+ // when getChildren() returns null, the node is
+ // not considered expanded.
+ mViewer.setExpandedState(entry, true);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ public void refreshEntry(final FileEntry entry) {
+ final Tree t = mViewer.getTree();
+ if (t != null && t.isDisposed() == false) {
+ Display display = t.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ public void run() {
+ if (t.isDisposed() == false) {
+ // refresh the entry.
+ mViewer.refresh(entry);
+ }
+ }
+ });
+ }
+ }
+ }
+ };
+
+ /**
+ *
+ */
+ public DeviceContentProvider() {
+ }
+
+ public void setListingService(FileListingService fls) {
+ mFileListingService = fls;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
+ */
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof FileEntry) {
+ FileEntry parentEntry = (FileEntry)parentElement;
+
+ Object[] oldEntries = parentEntry.getCachedChildren();
+ Object[] newEntries = mFileListingService.getChildren(parentEntry,
+ true, sListingReceiver);
+
+ if (newEntries != null) {
+ return newEntries;
+ } else {
+ // if null was returned, this means the cache was not valid,
+ // and a thread was launched for ls. sListingReceiver will be
+ // notified with the new entries.
+ return oldEntries;
+ }
+ }
+ return new Object[0];
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
+ */
+ public Object getParent(Object element) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ return entry.getParent();
+ }
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
+ */
+ public boolean hasChildren(Object element) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ return entry.getType() == FileListingService.TYPE_DIRECTORY;
+ }
+ return false;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
+ */
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof FileEntry) {
+ FileEntry entry = (FileEntry)inputElement;
+ if (entry.isRoot()) {
+ return getChildren(mRootEntry);
+ }
+ }
+
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IContentProvider#dispose()
+ */
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
+ */
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ if (viewer instanceof TreeViewer) {
+ mViewer = (TreeViewer)viewer;
+ }
+ if (newInput instanceof FileEntry) {
+ mRootEntry = (FileEntry)newInput;
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java
new file mode 100644
index 0000000..ba0f555
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java
@@ -0,0 +1,833 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.SyncService.SyncResult;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.Panel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.FileTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Device filesystem explorer class.
+ */
+public class DeviceExplorer extends Panel {
+
+ private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
+ private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
+
+ private static Pattern mKeyFilePattern = Pattern.compile(
+ "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
+ private static Pattern mDataFilePattern = Pattern.compile(
+ "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
+
+ public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
+ public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
+ public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
+ public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
+ public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
+ public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
+
+ private Composite mParent;
+ private TreeViewer mTreeViewer;
+ private Tree mTree;
+ private DeviceContentProvider mContentProvider;
+
+ private ICommonAction mPushAction;
+ private ICommonAction mPullAction;
+ private ICommonAction mDeleteAction;
+
+ private Image mFileImage;
+ private Image mFolderImage;
+ private Image mPackageImage;
+ private Image mOtherImage;
+
+ private Device mCurrentDevice;
+
+ private String mDefaultSave;
+
+ /**
+ * Implementation of the SyncService.ISyncProgressMonitor. It wraps a jFace IProgressMonitor
+ * and just forward the calls to the jFace object.
+ */
+ private static class SyncProgressMonitor implements ISyncProgressMonitor {
+
+ private IProgressMonitor mMonitor;
+ private String mName;
+
+ SyncProgressMonitor(IProgressMonitor monitor, String name) {
+ mMonitor = monitor;
+ mName = name;
+ }
+
+ public void start(int totalWork) {
+ mMonitor.beginTask(mName, totalWork);
+ }
+
+ public void stop() {
+ mMonitor.done();
+ }
+
+ public void advance(int work) {
+ mMonitor.worked(work);
+ }
+
+ public boolean isCanceled() {
+ return mMonitor.isCanceled();
+ }
+
+ public void startSubTask(String name) {
+ mMonitor.subTask(name);
+ }
+ }
+
+ public DeviceExplorer() {
+
+ }
+
+ /**
+ * Sets the images for the listview
+ * @param fileImage
+ * @param folderImage
+ * @param otherImage
+ */
+ public void setImages(Image fileImage, Image folderImage, Image packageImage,
+ Image otherImage) {
+ mFileImage = fileImage;
+ mFolderImage = folderImage;
+ mPackageImage = packageImage;
+ mOtherImage = otherImage;
+ }
+
+ /**
+ * Sets the actions so that the device explorer can enable/disable them based on the current
+ * selection
+ * @param pushAction
+ * @param pullAction
+ * @param deleteAction
+ */
+ public void setActions(ICommonAction pushAction, ICommonAction pullAction,
+ ICommonAction deleteAction) {
+ mPushAction = pushAction;
+ mPullAction = pullAction;
+ mDeleteAction = deleteAction;
+ }
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+ parent.setLayout(new FillLayout());
+
+ mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
+ mTree.setHeaderVisible(true);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // create columns
+ TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+ "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
+ "000000", COLUMN_SIZE, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
+ "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
+ "20:54", COLUMN_TIME, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
+ "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
+ TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
+ "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
+
+ // create the jface wrapper
+ mTreeViewer = new TreeViewer(mTree);
+
+ // setup data provider
+ mContentProvider = new DeviceContentProvider();
+ mTreeViewer.setContentProvider(mContentProvider);
+ mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
+ mFolderImage, mPackageImage, mOtherImage));
+
+ // setup a listener for selection
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection sel = event.getSelection();
+ if (sel.isEmpty()) {
+ mPullAction.setEnabled(false);
+ mPushAction.setEnabled(false);
+ mDeleteAction.setEnabled(false);
+ return;
+ }
+ if (sel instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) sel;
+ Object element = selection.getFirstElement();
+ if (element == null)
+ return;
+ if (element instanceof FileEntry) {
+ mPullAction.setEnabled(true);
+ mPushAction.setEnabled(selection.size() == 1);
+ if (selection.size() == 1) {
+ setDeleteEnabledState((FileEntry)element);
+ } else {
+ mDeleteAction.setEnabled(false);
+ }
+ }
+ }
+ }
+ });
+
+ // add support for double click
+ mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ ISelection sel = event.getSelection();
+
+ if (sel instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) sel;
+
+ if (selection.size() == 1) {
+ FileEntry entry = (FileEntry)selection.getFirstElement();
+ String name = entry.getName();
+
+ FileEntry parentEntry = entry.getParent();
+
+ // can't really do anything with no parent
+ if (parentEntry == null) {
+ return;
+ }
+
+ // check this is a file like we want.
+ Matcher m = mKeyFilePattern.matcher(name);
+ if (m.matches()) {
+ // get the name w/o the extension
+ String baseName = m.group(1);
+
+ // add the data extension
+ String dataName = baseName + TRACE_DATA_EXT;
+
+ FileEntry dataEntry = parentEntry.findChild(dataName);
+
+ handleTraceDoubleClick(baseName, entry, dataEntry);
+
+ } else {
+ m = mDataFilePattern.matcher(name);
+ if (m.matches()) {
+ // get the name w/o the extension
+ String baseName = m.group(1);
+
+ // add the key extension
+ String keyName = baseName + TRACE_KEY_EXT;
+
+ FileEntry keyEntry = parentEntry.findChild(keyName);
+
+ handleTraceDoubleClick(baseName, keyEntry, entry);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // setup drop listener
+ mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
+ new Transfer[] { FileTransfer.getInstance() },
+ new ViewerDropAdapter(mTreeViewer) {
+ @Override
+ public boolean performDrop(Object data) {
+ // get the item on which we dropped the item(s)
+ FileEntry target = (FileEntry)getCurrentTarget();
+
+ // in case we drop at the same level as root
+ if (target == null) {
+ return false;
+ }
+
+ // if the target is not a directory, we get the parent directory
+ if (target.isDirectory() == false) {
+ target = target.getParent();
+ }
+
+ if (target == null) {
+ return false;
+ }
+
+ // get the list of files to drop
+ String[] files = (String[])data;
+
+ // do the drop
+ pushFiles(files, target);
+
+ // we need to finish with a refresh
+ refresh(target);
+
+ return true;
+ }
+
+ @Override
+ public boolean validateDrop(Object target, int operation, TransferData transferType) {
+ if (target == null) {
+ return false;
+ }
+
+ // convert to the real item
+ FileEntry targetEntry = (FileEntry)target;
+
+ // if the target is not a directory, we get the parent directory
+ if (targetEntry.isDirectory() == false) {
+ target = targetEntry.getParent();
+ }
+
+ if (target == null) {
+ return false;
+ }
+
+ return true;
+ }
+ });
+
+ // create and start the refresh thread
+ new Thread("Device Ls refresher") {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ sleep(FileListingService.REFRESH_RATE);
+ } catch (InterruptedException e) {
+ return;
+ }
+
+ if (mTree != null && mTree.isDisposed() == false) {
+ Display display = mTree.getDisplay();
+ if (display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ mTreeViewer.refresh(true);
+ }
+ }
+ });
+ } else {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ }
+ }.start();
+
+ return mTree;
+ }
+
+ @Override
+ protected void postCreation() {
+
+ }
+
+ /**
+ * Sets the focus to the proper control inside the panel.
+ */
+ @Override
+ public void setFocus() {
+ mTree.setFocus();
+ }
+
+ /**
+ * Processes a double click on a trace file
+ * @param baseName the base name of the 2 files.
+ * @param keyEntry The FileEntry for the .key file.
+ * @param dataEntry The FileEntry for the .data file.
+ */
+ private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
+ FileEntry dataEntry) {
+ // first we need to download the files.
+ File keyFile;
+ File dataFile;
+ String path;
+ try {
+ // create a temp file for keyFile
+ File f = File.createTempFile(baseName, ".trace");
+ f.delete();
+ f.mkdir();
+
+ path = f.getAbsolutePath();
+
+ keyFile = new File(path + File.separator + keyEntry.getName());
+ dataFile = new File(path + File.separator + dataEntry.getName());
+ } catch (IOException e) {
+ return;
+ }
+
+ // download the files
+ SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
+ SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage()));
+ return;
+ }
+
+ result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage()));
+ return;
+ }
+
+ // now that we have the file, we need to launch traceview
+ String[] command = new String[2];
+ command[0] = DdmUiPreferences.getTraceview();
+ command[1] = path + File.separator + baseName;
+
+ try {
+ final Process p = Runtime.getRuntime().exec(command);
+
+ // create a thread for the output
+ new Thread("Traceview output") {
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(p.getErrorStream());
+ BufferedReader resultReader = new BufferedReader(is);
+
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ try {
+ while (true) {
+ String line = resultReader.readLine();
+ if (line != null) {
+ DdmConsole.printErrorToConsole("Traceview: " + line);
+ } else {
+ break;
+ }
+ }
+ // get the return code from the process
+ p.waitFor();
+ } catch (IOException e) {
+ } catch (InterruptedException e) {
+
+ }
+ }
+ }.start();
+
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * Pull the current selection on the local drive. This method displays
+ * a dialog box to let the user select where to store the file(s) and
+ * folder(s).
+ */
+ public void pullSelection() {
+ // get the selection
+ TreeItem[] items = mTree.getSelection();
+
+ // name of the single file pull, or null if we're pulling a directory
+ // or more than one object.
+ String filePullName = null;
+ FileEntry singleEntry = null;
+
+ // are we pulling a single file?
+ if (items.length == 1) {
+ singleEntry = (FileEntry)items[0].getData();
+ if (singleEntry.getType() == FileListingService.TYPE_FILE) {
+ filePullName = singleEntry.getName();
+ }
+ }
+
+ // where do we save by default?
+ String defaultPath = mDefaultSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+
+ if (filePullName != null) {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Get Device File");
+ fileDialog.setFileName(filePullName);
+ fileDialog.setFilterPath(defaultPath);
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ mDefaultSave = fileDialog.getFilterPath();
+
+ pullFile(singleEntry, fileName);
+ }
+ } else {
+ DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
+
+ directoryDialog.setText("Get Device Files/Folders");
+ directoryDialog.setFilterPath(defaultPath);
+
+ String directoryName = directoryDialog.open();
+ if (directoryName != null) {
+ pullSelection(items, directoryName);
+ }
+ }
+ }
+
+ /**
+ * Push new file(s) and folder(s) into the current selection. Current
+ * selection must be single item. If the current selection is not a
+ * directory, the parent directory is used.
+ * This method displays a dialog to let the user choose file to push to
+ * the device.
+ */
+ public void pushIntoSelection() {
+ // get the name of the object we're going to pull
+ TreeItem[] items = mTree.getSelection();
+
+ if (items.length == 0) {
+ return;
+ }
+
+ FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
+ String fileName;
+
+ dlg.setText("Put File on Device");
+
+ // There should be only one.
+ FileEntry entry = (FileEntry)items[0].getData();
+ dlg.setFileName(entry.getName());
+
+ String defaultPath = mDefaultSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+ dlg.setFilterPath(defaultPath);
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ mDefaultSave = dlg.getFilterPath();
+
+ // we need to figure out the remote path based on the current selection type.
+ String remotePath;
+ FileEntry toRefresh = entry;
+ if (entry.isDirectory()) {
+ remotePath = entry.getFullPath();
+ } else {
+ toRefresh = entry.getParent();
+ remotePath = toRefresh.getFullPath();
+ }
+
+ pushFile(fileName, remotePath);
+ mTreeViewer.refresh(toRefresh);
+ }
+ }
+
+ public void deleteSelection() {
+ // get the name of the object we're going to pull
+ TreeItem[] items = mTree.getSelection();
+
+ if (items.length != 1) {
+ return;
+ }
+
+ FileEntry entry = (FileEntry)items[0].getData();
+ final FileEntry parentEntry = entry.getParent();
+
+ // create the delete command
+ String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
+
+ try {
+ mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
+ public void addOutput(byte[] data, int offset, int length) {
+ // pass
+ // TODO get output to display errors if any.
+ }
+
+ public void flush() {
+ mTreeViewer.refresh(parentEntry);
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ // adb failed somehow, we do nothing. We should be displaying the error from the output
+ // of the shell command.
+ }
+
+ }
+
+ /**
+ * Force a full refresh of the explorer.
+ */
+ public void refresh() {
+ mTreeViewer.refresh(true);
+ }
+
+ /**
+ * Sets the new device to explorer
+ */
+ public void switchDevice(final Device device) {
+ if (device != mCurrentDevice) {
+ mCurrentDevice = device;
+ // now we change the input. but we need to do that in the
+ // ui thread.
+ if (mTree.isDisposed() == false) {
+ Display d = mTree.getDisplay();
+ d.asyncExec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ // new service
+ if (mCurrentDevice != null) {
+ FileListingService fls = mCurrentDevice.getFileListingService();
+ mContentProvider.setListingService(fls);
+ mTreeViewer.setInput(fls.getRoot());
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Refresh an entry from a non ui thread.
+ * @param entry the entry to refresh.
+ */
+ private void refresh(final FileEntry entry) {
+ Display d = mTreeViewer.getTree().getDisplay();
+ d.asyncExec(new Runnable() {
+ public void run() {
+ mTreeViewer.refresh(entry);
+ }
+ });
+ }
+
+ /**
+ * Pulls the selection from a device.
+ * @param items the tree selection the remote file on the device
+ * @param localDirector the local directory in which to save the files.
+ */
+ private void pullSelection(TreeItem[] items, final String localDirectory) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ // make a list of the FileEntry.
+ ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
+ for (TreeItem item : items) {
+ Object data = item.getData();
+ if (data instanceof FileEntry) {
+ entries.add((FileEntry)data);
+ }
+ }
+ final FileEntry[] entryArray = entries.toArray(
+ new FileEntry[entries.size()]);
+
+ // get a progressdialog
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ // create a monitor wrapper around the jface monitor
+ SyncResult result = sync.pull(entryArray, localDirectory,
+ new SyncProgressMonitor(monitor,
+ "Pulling file(s) from the device"));
+
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull selection: %1$s", result.getMessage()));
+ }
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Pulls a file from a device.
+ * @param remote the remote file on the device
+ * @param local the destination filepath
+ */
+ private void pullFile(final FileEntry remote, final String local) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor(
+ monitor, String.format("Pulling %1$s from the device",
+ remote.getName())));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to pull %1$s: %2$s", remote, result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole( "Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to pull selection");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Pushes several files and directory into a remote directory.
+ * @param localFiles
+ * @param remoteDirectory
+ */
+ private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ SyncResult result = sync.push(localFiles, remoteDirectory,
+ new SyncProgressMonitor(monitor,
+ "Pushing file(s) to the device"));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push the items: %1$s", result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole("Failed to push the items");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to push the items");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ return;
+ }
+ }
+
+ /**
+ * Pushes a file on a device.
+ * @param local the local filepath of the file to push
+ * @param remoteDirectory the remote destination directory on the device
+ */
+ private void pushFile(final String local, final String remoteDirectory) {
+ final SyncService sync = mCurrentDevice.getSyncService();
+ if (sync != null) {
+ try {
+ new ProgressMonitorDialog(mParent.getShell()).run(true, true,
+ new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor)
+ throws InvocationTargetException,
+ InterruptedException {
+ // get the file name
+ String[] segs = local.split(Pattern.quote(File.separator));
+ String name = segs[segs.length-1];
+ String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
+ + name;
+
+ SyncResult result = sync.pushFile(local, remoteFile,
+ new SyncProgressMonitor(monitor,
+ String.format("Pushing %1$s to the device.", name)));
+ if (result.getCode() != SyncService.RESULT_OK) {
+ DdmConsole.printErrorToConsole(String.format(
+ "Failed to push %1$s on %2$s: %3$s",
+ name, mCurrentDevice.getSerialNumber(), result.getMessage()));
+ }
+
+ sync.close();
+ }
+ });
+ } catch (InvocationTargetException e) {
+ DdmConsole.printErrorToConsole("Failed to push the item(s).");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ } catch (InterruptedException e) {
+ DdmConsole.printErrorToConsole("Failed to push the item(s).");
+ DdmConsole.printErrorToConsole(e.getMessage());
+ }
+ return;
+ }
+ }
+
+ /**
+ * Sets the enabled state based on a FileEntry properties
+ * @param element The selected FileEntry
+ */
+ protected void setDeleteEnabledState(FileEntry element) {
+ mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java
new file mode 100644
index 0000000..1dca962
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Label provider for the FileEntry.
+ */
+class FileLabelProvider implements ILabelProvider, ITableLabelProvider {
+
+ private Image mFileImage;
+ private Image mFolderImage;
+ private Image mPackageImage;
+ private Image mOtherImage;
+
+ /**
+ * Creates Label provider with custom images.
+ * @param fileImage the Image to represent a file
+ * @param folderImage the Image to represent a folder
+ * @param packageImage the Image to represent a .apk file. If null,
+ * fileImage is used instead.
+ * @param otherImage the Image to represent all other entry type.
+ */
+ public FileLabelProvider(Image fileImage, Image folderImage,
+ Image packageImage, Image otherImage) {
+ mFileImage = fileImage;
+ mFolderImage = folderImage;
+ mOtherImage = otherImage;
+ if (packageImage != null) {
+ mPackageImage = packageImage;
+ } else {
+ mPackageImage = fileImage;
+ }
+ }
+
+ /**
+ * Creates a label provider with default images.
+ *
+ */
+ public FileLabelProvider() {
+
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
+ */
+ public Image getImage(Object element) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
+ */
+ public String getText(Object element) {
+ return null;
+ }
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == 0) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+ switch (entry.getType()) {
+ case FileListingService.TYPE_FILE:
+ case FileListingService.TYPE_LINK:
+ // get the name and extension
+ if (entry.isApplicationPackage()) {
+ return mPackageImage;
+ }
+ return mFileImage;
+ case FileListingService.TYPE_DIRECTORY:
+ case FileListingService.TYPE_DIRECTORY_LINK:
+ return mFolderImage;
+ }
+ }
+
+ // default case return a different image.
+ return mOtherImage;
+ }
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof FileEntry) {
+ FileEntry entry = (FileEntry)element;
+
+ switch (columnIndex) {
+ case 0:
+ return entry.getName();
+ case 1:
+ return entry.getSize();
+ case 2:
+ return entry.getDate();
+ case 3:
+ return entry.getTime();
+ case 4:
+ return entry.getPermissions();
+ case 5:
+ return entry.getInfo();
+ }
+ }
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener)
+ */
+ public void addListener(ILabelProviderListener listener) {
+ // we don't need listeners.
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
+ */
+ public void dispose() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
+ */
+ public boolean isLabelProperty(Object element, String property) {
+ return false;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
+ */
+ public void removeListener(ILabelProviderListener listener) {
+ // we don't need listeners
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java
new file mode 100644
index 0000000..578a7ac
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Encapsulation of controls handling a location coordinate in decimal and sexagesimal.
+ * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener}
+ * on all the {@link Text} widgets.
+ * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by
+ * a call to {@link #isValueValid()})
+ */
+public final class CoordinateControls {
+ private double mValue;
+ private boolean mValueValidity = false;
+ private Text mDecimalText;
+ private Text mSexagesimalDegreeText;
+ private Text mSexagesimalMinuteText;
+ private Text mSexagesimalSecondText;
+
+ /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)}
+ * is called. This is an int instead of a boolean to act as a counter. */
+ private int mManualTextChange = 0;
+
+ /**
+ * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode.
+ */
+ private ModifyListener mSexagesimalListener = new ModifyListener() {
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = getValueFromSexagesimalControls();
+ setValueIntoDecimalControl(mValue);
+ mValueValidity = true;
+ } catch (NumberFormatException e) {
+ // wrong format empty the decimal controls.
+ mValueValidity = false;
+ resetDecimalControls();
+ }
+ }
+ };
+
+ /**
+ * Creates the {@link Text} control for the decimal display of the coordinate.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createDecimalText(Composite parent) {
+ mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() {
+ public void modifyText(ModifyEvent event) {
+ if (mManualTextChange > 0) {
+ return;
+ }
+ try {
+ mValue = Double.parseDouble(mDecimalText.getText());
+ setValueIntoSexagesimalControl(mValue);
+ mValueValidity = true;
+ } catch (NumberFormatException e) {
+ // wrong format empty the sexagesimal controls.
+ mValueValidity = false;
+ resetSexagesimalControls();
+ }
+ }
+ });
+ }
+
+ /**
+ * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalDegreeText(Composite parent) {
+ mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalMinuteText(Composite parent) {
+ mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal
+ * mode.
+ * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+ * @param parent The {@link Composite} parent of the control.
+ */
+ public void createSexagesimalSecondText(Composite parent) {
+ mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$
+ }
+
+ /**
+ * Sets the coordinate into the {@link Text} controls.
+ * @param value the coordinate value to set.
+ */
+ public void setValue(double value) {
+ mValue = value;
+ mValueValidity = true;
+ setValueIntoDecimalControl(value);
+ setValueIntoSexagesimalControl(value);
+ }
+
+ /**
+ * Returns whether the value in the control(s) is valid.
+ */
+ public boolean isValueValid() {
+ return mValueValidity;
+ }
+
+ /**
+ * Returns the current value set in the control(s).
+ * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed
+ * before any call to this method.
+ */
+ public double getValue() {
+ return mValue;
+ }
+
+ /**
+ * Enables or disables all the {@link Text} controls.
+ * @param enabled the enabled state.
+ */
+ public void setEnabled(boolean enabled) {
+ mDecimalText.setEnabled(enabled);
+ mSexagesimalDegreeText.setEnabled(enabled);
+ mSexagesimalMinuteText.setEnabled(enabled);
+ mSexagesimalSecondText.setEnabled(enabled);
+ }
+
+ private void resetDecimalControls() {
+ mManualTextChange++;
+ mDecimalText.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+
+ private void resetSexagesimalControls() {
+ mManualTextChange++;
+ mSexagesimalDegreeText.setText(""); //$NON-NLS-1$
+ mSexagesimalMinuteText.setText(""); //$NON-NLS-1$
+ mSexagesimalSecondText.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+
+ /**
+ * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener}
+ * @param parent the parent {@link Composite}.
+ * @param defaultString the default string to be used to compute the {@link Text} control
+ * size hint.
+ * @param listener the {@link ModifyListener} to be called when the {@link Text} control is
+ * modified.
+ */
+ private Text createTextControl(Composite parent, String defaultString,
+ ModifyListener listener) {
+ // create the control
+ Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+
+ // add the standard listener to it.
+ text.addModifyListener(listener);
+
+ // compute its size/
+ mManualTextChange++;
+ text.setText(defaultString);
+ text.pack();
+ Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ text.setText(""); //$NON-NLS-1$
+ mManualTextChange--;
+
+ GridData gridData = new GridData();
+ gridData.widthHint = size.x;
+ text.setLayoutData(gridData);
+
+ return text;
+ }
+
+ private double getValueFromSexagesimalControls() throws NumberFormatException {
+ double degrees = Double.parseDouble(mSexagesimalDegreeText.getText());
+ double minutes = Double.parseDouble(mSexagesimalMinuteText.getText());
+ double seconds = Double.parseDouble(mSexagesimalSecondText.getText());
+
+ boolean isPositive = (degrees >= 0.);
+ degrees = Math.abs(degrees);
+
+ double value = degrees + minutes / 60. + seconds / 3600.;
+ return isPositive ? value : - value;
+ }
+
+ private void setValueIntoDecimalControl(double value) {
+ mManualTextChange++;
+ mDecimalText.setText(String.format("%.6f", value));
+ mManualTextChange--;
+ }
+
+ private void setValueIntoSexagesimalControl(double value) {
+ // get the sign and make the number positive no matter what.
+ boolean isPositive = (value >= 0.);
+ value = Math.abs(value);
+
+ // get the degree
+ double degrees = Math.floor(value);
+
+ // get the minutes
+ double minutes = Math.floor((value - degrees) * 60.);
+
+ // get the seconds.
+ double seconds = (value - degrees) * 3600. - minutes * 60.;
+
+ mManualTextChange++;
+ mSexagesimalDegreeText.setText(
+ Integer.toString(isPositive ? (int)degrees : (int)- degrees));
+ mSexagesimalMinuteText.setText(Integer.toString((int)minutes));
+ mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$
+ mManualTextChange--;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java
new file mode 100644
index 0000000..a30337a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic GPX parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic waypoint information, and tracks (merging segments).
+ */
+public class GpxParser {
+
+ private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$
+
+ private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$
+ private final static String NODE_TRACK = "trk"; //$NON-NLS-1$
+ private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$
+ private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$
+ private final static String NODE_NAME = "name"; //$NON-NLS-1$
+ private final static String NODE_TIME = "time"; //$NON-NLS-1$
+ private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$
+ private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$
+ private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$
+ private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$
+
+ private static SAXParserFactory sParserFactory;
+
+ static {
+ sParserFactory = SAXParserFactory.newInstance();
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ private String mFileName;
+
+ private GpxHandler mHandler;
+
+ /** Pattern to parse time with optional sub-second precision, and optional
+ * Z indicating the time is in UTC. */
+ private final static Pattern ISO8601_TIME =
+ Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$
+
+ /**
+ * Handler for the SAX parser.
+ */
+ private static class GpxHandler extends DefaultHandler {
+ // --------- parsed data ---------
+ List<WayPoint> mWayPoints;
+ List<Track> mTrackList;
+
+ // --------- state for parsing ---------
+ Track mCurrentTrack;
+ TrackPoint mCurrentTrackPoint;
+ WayPoint mCurrentWayPoint;
+ final StringBuilder mStringAccumulator = new StringBuilder();
+
+ boolean mSuccess = true;
+
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ // we only care about the standard GPX nodes.
+ try {
+ if (NS_GPX.equals(uri)) {
+ if (NODE_WAYPOINT.equals(localName)) {
+ if (mWayPoints == null) {
+ mWayPoints = new ArrayList<WayPoint>();
+ }
+
+ mWayPoints.add(mCurrentWayPoint = new WayPoint());
+ handleLocation(mCurrentWayPoint, attributes);
+ } else if (NODE_TRACK.equals(localName)) {
+ if (mTrackList == null) {
+ mTrackList = new ArrayList<Track>();
+ }
+
+ mTrackList.add(mCurrentTrack = new Track());
+ } else if (NODE_TRACK_SEGMENT.equals(localName)) {
+ // for now we do nothing here. This will merge all the segments into
+ // a single TrackPoint list in the Track.
+ } else if (NODE_TRACK_POINT.equals(localName)) {
+ if (mCurrentTrack != null) {
+ mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint());
+ handleLocation(mCurrentTrackPoint, attributes);
+ }
+ }
+ }
+ } finally {
+ // no matter the node, we empty the StringBuilder accumulator when we start
+ // a new node.
+ mStringAccumulator.setLength(0);
+ }
+ }
+
+ /**
+ * Processes new characters for the node content. The characters are simply stored,
+ * and will be processed when {@link #endElement(String, String, String)} is called.
+ */
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mStringAccumulator.append(ch, start, length);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+ if (NS_GPX.equals(uri)) {
+ if (NODE_WAYPOINT.equals(localName)) {
+ mCurrentWayPoint = null;
+ } else if (NODE_TRACK.equals(localName)) {
+ mCurrentTrack = null;
+ } else if (NODE_TRACK_POINT.equals(localName)) {
+ mCurrentTrackPoint = null;
+ } else if (NODE_NAME.equals(localName)) {
+ if (mCurrentTrack != null) {
+ mCurrentTrack.setName(mStringAccumulator.toString());
+ } else if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setName(mStringAccumulator.toString());
+ }
+ } else if (NODE_TIME.equals(localName)) {
+ if (mCurrentTrackPoint != null) {
+ mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString()));
+ }
+ } else if (NODE_ELEVATION.equals(localName)) {
+ if (mCurrentTrackPoint != null) {
+ mCurrentTrackPoint.setElevation(
+ Double.parseDouble(mStringAccumulator.toString()));
+ } else if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setElevation(
+ Double.parseDouble(mStringAccumulator.toString()));
+ }
+ } else if (NODE_DESCRIPTION.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setDescription(mStringAccumulator.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ /**
+ * Converts the string description of the time into milliseconds since epoch.
+ * @param timeString the string data.
+ * @return date in milliseconds.
+ */
+ private long computeTime(String timeString) {
+ // Time looks like: 2008-04-05T19:24:50Z
+ Matcher m = ISO8601_TIME.matcher(timeString);
+ if (m.matches()) {
+ // get the various elements and reconstruct time as a long.
+ try {
+ int year = Integer.parseInt(m.group(1));
+ int month = Integer.parseInt(m.group(2));
+ int date = Integer.parseInt(m.group(3));
+ int hourOfDay = Integer.parseInt(m.group(4));
+ int minute = Integer.parseInt(m.group(5));
+ int second = Integer.parseInt(m.group(6));
+
+ // handle the optional parameters.
+ int milliseconds = 0;
+
+ String subSecondGroup = m.group(7);
+ if (subSecondGroup != null) {
+ milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup));
+ }
+
+ boolean utcTime = m.group(8) != null;
+
+ // now we convert into milliseconds since epoch.
+ Calendar c;
+ if (utcTime) {
+ c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
+ } else {
+ c = Calendar.getInstance();
+ }
+
+ c.set(year, month, date, hourOfDay, minute, second);
+
+ return c.getTimeInMillis() + milliseconds;
+ } catch (NumberFormatException e) {
+ // format is invalid, we'll return -1 below.
+ }
+
+ }
+
+ // invalid time!
+ return -1;
+ }
+
+ /**
+ * Handles the location attributes and store them into a {@link LocationPoint}.
+ * @param locationNode the {@link LocationPoint} to receive the location data.
+ * @param attributes the attributes from the XML node.
+ */
+ private void handleLocation(LocationPoint locationNode, Attributes attributes) {
+ try {
+ double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE));
+ double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE));
+
+ locationNode.setLocation(longitude, latitude);
+ } catch (NumberFormatException e) {
+ // wrong data, do nothing.
+ }
+ }
+
+ WayPoint[] getWayPoints() {
+ if (mWayPoints != null) {
+ return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+ }
+
+ return null;
+ }
+
+ Track[] getTracks() {
+ if (mTrackList != null) {
+ return mTrackList.toArray(new Track[mTrackList.size()]);
+ }
+
+ return null;
+ }
+
+ boolean getSuccess() {
+ return mSuccess;
+ }
+ }
+
+ /**
+ * A GPS track.
+ * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment.
+ */
+ public final static class Track {
+ private String mName;
+ private String mComment;
+ private List<TrackPoint> mPoints = new ArrayList<TrackPoint>();
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ void setComment(String comment) {
+ mComment = comment;
+ }
+
+ public String getComment() {
+ return mComment;
+ }
+
+ void addPoint(TrackPoint trackPoint) {
+ mPoints.add(trackPoint);
+ }
+
+ public TrackPoint[] getPoints() {
+ return mPoints.toArray(new TrackPoint[mPoints.size()]);
+ }
+
+ public long getFirstPointTime() {
+ if (mPoints.size() > 0) {
+ return mPoints.get(0).getTime();
+ }
+
+ return -1;
+ }
+
+ public long getLastPointTime() {
+ if (mPoints.size() > 0) {
+ return mPoints.get(mPoints.size()-1).getTime();
+ }
+
+ return -1;
+ }
+
+ public int getPointCount() {
+ return mPoints.size();
+ }
+ }
+
+ /**
+ * Creates a new GPX parser for a file specified by its full path.
+ * @param fileName The full path of the GPX file to parse.
+ */
+ public GpxParser(String fileName) {
+ mFileName = fileName;
+ }
+
+ /**
+ * Parses the GPX file.
+ * @return <code>true</code> if success.
+ */
+ public boolean parse() {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ mHandler = new GpxHandler();
+
+ parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+
+ return mHandler.getSuccess();
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public WayPoint[] getWayPoints() {
+ if (mHandler != null) {
+ return mHandler.getWayPoints();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public Track[] getTracks() {
+ if (mHandler != null) {
+ return mHandler.getTracks();
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java
new file mode 100644
index 0000000..af485ac
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic KML parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic Placemark information.
+ */
+public class KmlParser {
+
+ private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$
+
+ private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$
+ private final static String NODE_NAME = "name"; //$NON-NLS-1$
+ private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$
+
+ private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?");
+
+ private static SAXParserFactory sParserFactory;
+
+ static {
+ sParserFactory = SAXParserFactory.newInstance();
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ private String mFileName;
+
+ private KmlHandler mHandler;
+
+ /**
+ * Handler for the SAX parser.
+ */
+ private static class KmlHandler extends DefaultHandler {
+ // --------- parsed data ---------
+ List<WayPoint> mWayPoints;
+
+ // --------- state for parsing ---------
+ WayPoint mCurrentWayPoint;
+ final StringBuilder mStringAccumulator = new StringBuilder();
+
+ boolean mSuccess = true;
+
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ // we only care about the standard GPX nodes.
+ try {
+ if (uri.startsWith(NS_KML_2)) {
+ if (NODE_PLACEMARK.equals(localName)) {
+ if (mWayPoints == null) {
+ mWayPoints = new ArrayList<WayPoint>();
+ }
+
+ mWayPoints.add(mCurrentWayPoint = new WayPoint());
+ }
+ }
+ } finally {
+ // no matter the node, we empty the StringBuilder accumulator when we start
+ // a new node.
+ mStringAccumulator.setLength(0);
+ }
+ }
+
+ /**
+ * Processes new characters for the node content. The characters are simply stored,
+ * and will be processed when {@link #endElement(String, String, String)} is called.
+ */
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mStringAccumulator.append(ch, start, length);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+ if (uri.startsWith(NS_KML_2)) {
+ if (NODE_PLACEMARK.equals(localName)) {
+ mCurrentWayPoint = null;
+ } else if (NODE_NAME.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ mCurrentWayPoint.setName(mStringAccumulator.toString());
+ }
+ } else if (NODE_COORDINATES.equals(localName)) {
+ if (mCurrentWayPoint != null) {
+ parseLocation(mCurrentWayPoint, mStringAccumulator.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ mSuccess = false;
+ }
+
+ /**
+ * Parses the location string and store the information into a {@link LocationPoint}.
+ * @param locationNode the {@link LocationPoint} to receive the location data.
+ * @param location The string containing the location info.
+ */
+ private void parseLocation(LocationPoint locationNode, String location) {
+ Matcher m = sLocationPattern.matcher(location);
+ if (m.matches()) {
+ try {
+ double longitude = Double.parseDouble(m.group(1));
+ double latitude = Double.parseDouble(m.group(2));
+
+ locationNode.setLocation(longitude, latitude);
+
+ if (m.groupCount() == 3) {
+ // looks like we have elevation data.
+ locationNode.setElevation(Double.parseDouble(m.group(3)));
+ }
+ } catch (NumberFormatException e) {
+ // wrong data, do nothing.
+ }
+ }
+ }
+
+ WayPoint[] getWayPoints() {
+ if (mWayPoints != null) {
+ return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+ }
+
+ return null;
+ }
+
+ boolean getSuccess() {
+ return mSuccess;
+ }
+ }
+
+ /**
+ * Creates a new GPX parser for a file specified by its full path.
+ * @param fileName The full path of the GPX file to parse.
+ */
+ public KmlParser(String fileName) {
+ mFileName = fileName;
+ }
+
+ /**
+ * Parses the GPX file.
+ * @return <code>true</code> if success.
+ */
+ public boolean parse() {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ mHandler = new KmlHandler();
+
+ parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+
+ return mHandler.getSuccess();
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+ * if the parsing failed.
+ */
+ public WayPoint[] getWayPoints() {
+ if (mHandler != null) {
+ return mHandler.getWayPoints();
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java
new file mode 100644
index 0000000..dbb8f41
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+/**
+ * Base class for Location aware points.
+ */
+class LocationPoint {
+ private double mLongitude;
+ private double mLatitude;
+ private boolean mHasElevation = false;
+ private double mElevation;
+
+ final void setLocation(double longitude, double latitude) {
+ mLongitude = longitude;
+ mLatitude = latitude;
+ }
+
+ public final double getLongitude() {
+ return mLongitude;
+ }
+
+ public final double getLatitude() {
+ return mLatitude;
+ }
+
+ final void setElevation(double elevation) {
+ mElevation = elevation;
+ mHasElevation = true;
+ }
+
+ public final boolean hasElevation() {
+ return mHasElevation;
+ }
+
+ public final double getElevation() {
+ return mElevation;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java
new file mode 100644
index 0000000..7fb37ce
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link Track} objects in a Table.
+ * <p/>The expected type for the input is {@link Track}<code>[]</code>.
+ */
+public class TrackContentProvider implements IStructuredContentProvider {
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Track[]) {
+ return (Track[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java
new file mode 100644
index 0000000..81d1f7d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link Track} objects.
+ */
+public class TrackLabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof Track) {
+ Track track = (Track)element;
+ switch (columnIndex) {
+ case 0:
+ return track.getName();
+ case 1:
+ return Integer.toString(track.getPointCount());
+ case 2:
+ long time = track.getFirstPointTime();
+ if (time != -1) {
+ return new Date(time).toString();
+ }
+ break;
+ case 3:
+ time = track.getLastPointTime();
+ if (time != -1) {
+ return new Date(time).toString();
+ }
+ break;
+ case 4:
+ return track.getComment();
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java
new file mode 100644
index 0000000..527f4bf
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+
+/**
+ * A Track Point.
+ * <p/>A track point is a point in time and space.
+ */
+public class TrackPoint extends LocationPoint {
+ private long mTime;
+
+ void setTime(long time) {
+ mTime = time;
+ }
+
+ public long getTime() {
+ return mTime;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java
new file mode 100644
index 0000000..32880bd
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+/**
+ * A GPS/KML way point.
+ * <p/>A waypoint is a user specified location, with a name and an optional description.
+ */
+public final class WayPoint extends LocationPoint {
+ private String mName;
+ private String mDescription;
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ void setDescription(String description) {
+ mDescription = description;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java
new file mode 100644
index 0000000..fced777
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link WayPoint} objects in a Table.
+ * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>.
+ */
+public class WayPointContentProvider implements IStructuredContentProvider {
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof WayPoint[]) {
+ return (WayPoint[])inputElement;
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java
new file mode 100644
index 0000000..f5e6f1b
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.location;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link WayPoint} objects.
+ */
+public class WayPointLabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof WayPoint) {
+ WayPoint wayPoint = (WayPoint)element;
+ switch (columnIndex) {
+ case 0:
+ return wayPoint.getName();
+ case 1:
+ return String.format("%.6f", wayPoint.getLongitude());
+ case 2:
+ return String.format("%.6f", wayPoint.getLatitude());
+ case 3:
+ if (wayPoint.hasElevation()) {
+ return String.format("%.1f", wayPoint.getElevation());
+ } else {
+ return "-";
+ }
+ case 4:
+ return wayPoint.getDescription();
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java
new file mode 100644
index 0000000..9de1ac7
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class BugReportImporter {
+
+ private final static String TAG_HEADER = "------ EVENT LOG TAGS ------";
+ private final static String LOG_HEADER = "------ EVENT LOG ------";
+ private final static String HEADER_TAG = "------";
+
+ private String[] mTags;
+ private String[] mLog;
+
+ public BugReportImporter(String filePath) throws FileNotFoundException {
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(filePath)));
+
+ try {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (TAG_HEADER.equals(line)) {
+ readTags(reader);
+ return;
+ }
+ }
+ } catch (IOException e) {
+ }
+ }
+
+ public String[] getTags() {
+ return mTags;
+ }
+
+ public String[] getLog() {
+ return mLog;
+ }
+
+ private void readTags(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ if (LOG_HEADER.equals(line)) {
+ mTags = content.toArray(new String[content.size()]);
+ readLog(reader);
+ return;
+ } else {
+ content.add(line);
+ }
+ }
+ }
+
+ private void readLog(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith(HEADER_TAG) == false) {
+ content.add(line);
+ } else {
+ break;
+ }
+ }
+
+ mLog = content.toArray(new String[content.size()]);
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java
new file mode 100644
index 0000000..473387a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+
+import java.util.ArrayList;
+
+public class DisplayFilteredLog extends DisplayLog {
+
+ public DisplayFilteredLog(String name) {
+ super(name);
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ ArrayList<ValueDisplayDescriptor> valueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+ addToLog(event, logParser, valueDescriptors, occurrenceDescriptors);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_FILTERED_LOG;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java
new file mode 100644
index 0000000..0cffd7e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DisplayGraph extends EventDisplay {
+
+ public DisplayGraph(String name) {
+ super(name);
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ Collection<TimeSeriesCollection> datasets = mValueTypeDataSetMap.values();
+ for (TimeSeriesCollection dataset : datasets) {
+ dataset.removeAllSeries();
+ }
+ if (mOccurrenceDataSet != null) {
+ mOccurrenceDataSet.removeAllSeries();
+ }
+ mValueDescriptorSeriesMap.clear();
+ mOcurrenceDescriptorSeriesMap.clear();
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ String title = getChartTitle(logParser);
+ return createCompositeChart(parent, logParser, title);
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ ArrayList<ValueDisplayDescriptor> valueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+ updateChart(event, logParser, valueDescriptors, occurrenceDescriptors);
+ }
+ }
+
+ /**
+ * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined
+ * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from
+ * the two lists.
+ * <p/>This method is only called when at least one of the descriptor list is non empty.
+ * @param event
+ * @param logParser
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ */
+ private void updateChart(EventContainer event, EventLogParser logParser,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+ Map<Integer, String> tagMap = logParser.getTagMap();
+
+ Millisecond millisecondTime = null;
+ long msec = -1;
+
+ // If the event container is a cpu container (tag == 2721), and there is no descriptor
+ // for the total CPU load, then we do accumulate all the values.
+ boolean accumulateValues = false;
+ double accumulatedValue = 0;
+
+ if (event.mTag == 2721) {
+ accumulateValues = true;
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ accumulateValues &= (descriptor.valueIndex != 0);
+ }
+ }
+
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ try {
+ // get the hashmap for this descriptor
+ HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor);
+
+ // if it's not there yet, we create it.
+ if (map == null) {
+ map = new HashMap<Integer, TimeSeries>();
+ mValueDescriptorSeriesMap.put(descriptor, map);
+ }
+
+ // get the TimeSeries for this pid
+ TimeSeries timeSeries = map.get(event.pid);
+
+ // if it doesn't exist yet, we create it
+ if (timeSeries == null) {
+ // get the series name
+ String seriesFullName = null;
+ String seriesLabel = getSeriesLabel(event, descriptor);
+
+ switch (mValueDescriptorCheck) {
+ case EVENT_CHECK_SAME_TAG:
+ seriesFullName = String.format("%1$s / %2$s", seriesLabel,
+ descriptor.valueName);
+ break;
+ case EVENT_CHECK_SAME_VALUE:
+ seriesFullName = String.format("%1$s", seriesLabel);
+ break;
+ default:
+ seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel,
+ tagMap.get(descriptor.eventTag),
+ descriptor.valueName);
+ break;
+ }
+
+ // get the data set for this ValueType
+ TimeSeriesCollection dataset = getValueDataset(
+ logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex]
+ .getValueType(),
+ accumulateValues);
+
+ // create the series
+ timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+ if (mMaximumChartItemAge != -1) {
+ timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000);
+ }
+
+ dataset.addSeries(timeSeries);
+
+ // add it to the map.
+ map.put(event.pid, timeSeries);
+ }
+
+ // update the timeSeries.
+
+ // get the value from the event
+ double value = event.getValueAsDouble(descriptor.valueIndex);
+
+ // accumulate the values if needed.
+ if (accumulateValues) {
+ accumulatedValue += value;
+ value = accumulatedValue;
+ }
+
+ // get the time
+ if (millisecondTime == null) {
+ msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ millisecondTime = new Millisecond(new Date(msec));
+ }
+
+ // add the value to the time series
+ timeSeries.addOrUpdate(millisecondTime, value);
+ } catch (InvalidTypeException e) {
+ // just ignore this descriptor if there's a type mismatch
+ }
+ }
+
+ for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) {
+ try {
+ // get the hashmap for this descriptor
+ HashMap<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor);
+
+ // if it's not there yet, we create it.
+ if (map == null) {
+ map = new HashMap<Integer, TimeSeries>();
+ mOcurrenceDescriptorSeriesMap.put(descriptor, map);
+ }
+
+ // get the TimeSeries for this pid
+ TimeSeries timeSeries = map.get(event.pid);
+
+ // if it doesn't exist yet, we create it.
+ if (timeSeries == null) {
+ String seriesLabel = getSeriesLabel(event, descriptor);
+
+ String seriesFullName = String.format("[%1$s:%2$s]",
+ tagMap.get(descriptor.eventTag), seriesLabel);
+
+ timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+ if (mMaximumChartItemAge != -1) {
+ timeSeries.setMaximumItemAge(mMaximumChartItemAge);
+ }
+
+ getOccurrenceDataSet().addSeries(timeSeries);
+
+ map.put(event.pid, timeSeries);
+ }
+
+ // update the series
+
+ // get the time
+ if (millisecondTime == null) {
+ msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ millisecondTime = new Millisecond(new Date(msec));
+ }
+
+ // add the value to the time series
+ timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused
+ } catch (InvalidTypeException e) {
+ // just ignore this descriptor if there's a type mismatch
+ }
+ }
+
+ // go through all the series and remove old values.
+ if (msec != -1 && mMaximumChartItemAge != -1) {
+ Collection<HashMap<Integer, TimeSeries>> pidMapValues =
+ mValueDescriptorSeriesMap.values();
+
+ for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+ Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+ for (TimeSeries timeSeries : seriesCollection) {
+ timeSeries.removeAgedItems(msec, true);
+ }
+ }
+
+ pidMapValues = mOcurrenceDescriptorSeriesMap.values();
+ for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+ Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+ for (TimeSeries timeSeries : seriesCollection) {
+ timeSeries.removeAgedItems(msec, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}.
+ * If the data set is not yet created, it is first allocated and set up into the
+ * {@link org.jfree.chart.JFreeChart} object.
+ * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set.
+ * @param accumulateValues
+ */
+ private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) {
+ TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type);
+ if (dataset == null) {
+ // create the data set and store it in the map
+ dataset = new TimeSeriesCollection();
+ mValueTypeDataSetMap.put(type, dataset);
+
+ // create the renderer and configure it depending on the ValueType
+ AbstractXYItemRenderer renderer;
+ if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) {
+ renderer = new XYAreaRenderer();
+ } else {
+ XYLineAndShapeRenderer r = new XYLineAndShapeRenderer();
+ r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT);
+
+ renderer = r;
+ }
+
+ // set both the dataset and the renderer in the plot object.
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setDataset(mDataSetCount, dataset);
+ xyPlot.setRenderer(mDataSetCount, renderer);
+
+ // put a new axis label, and configure it.
+ NumberAxis axis = new NumberAxis(type.toString());
+
+ if (type == EventValueDescription.ValueType.PERCENT) {
+ // force percent range to be (0,100) fixed.
+ axis.setAutoRange(false);
+ axis.setRange(0., 100.);
+ }
+
+ // for the index, we ignore the occurrence dataset
+ int count = mDataSetCount;
+ if (mOccurrenceDataSet != null) {
+ count--;
+ }
+
+ xyPlot.setRangeAxis(count, axis);
+ if ((count % 2) == 0) {
+ xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT);
+ } else {
+ xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT);
+ }
+
+ // now we link the dataset and the axis
+ xyPlot.mapDatasetToRangeAxis(mDataSetCount, count);
+
+ mDataSetCount++;
+ }
+
+ return dataset;
+ }
+
+ /**
+ * Return the series label for this event. This only contains the pid information.
+ * @param event the {@link EventContainer}
+ * @param descriptor the {@link OccurrenceDisplayDescriptor}
+ * @return the series label.
+ * @throws InvalidTypeException
+ */
+ private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor)
+ throws InvalidTypeException {
+ if (descriptor.seriesValueIndex != -1) {
+ if (descriptor.includePid == false) {
+ return event.getValueAsString(descriptor.seriesValueIndex);
+ } else {
+ return String.format("%1$s (%2$d)",
+ event.getValueAsString(descriptor.seriesValueIndex), event.pid);
+ }
+ }
+
+ return Integer.toString(event.pid);
+ }
+
+ /**
+ * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not
+ * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object.
+ */
+ private TimeSeriesCollection getOccurrenceDataSet() {
+ if (mOccurrenceDataSet == null) {
+ mOccurrenceDataSet = new TimeSeriesCollection();
+
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet);
+
+ OccurrenceRenderer renderer = new OccurrenceRenderer();
+ renderer.setBaseShapesVisible(false);
+ xyPlot.setRenderer(mDataSetCount, renderer);
+
+ mDataSetCount++;
+ }
+
+ return mOccurrenceDataSet;
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_GRAPH;
+ }
+
+ /**
+ * Sets the current {@link EventLogParser} object.
+ */
+ @Override
+ protected void setNewLogParser(EventLogParser logParser) {
+ if (mChart != null) {
+ mChart.setTitle(getChartTitle(logParser));
+ }
+ }
+ /**
+ * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}.
+ *
+ * @param logParser the logParser.
+ * @return the chart title.
+ */
+ private String getChartTitle(EventLogParser logParser) {
+ if (mValueDescriptors.size() > 0) {
+ String chartDesc = null;
+ switch (mValueDescriptorCheck) {
+ case EVENT_CHECK_SAME_TAG:
+ if (logParser != null) {
+ chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag);
+ }
+ break;
+ case EVENT_CHECK_SAME_VALUE:
+ if (logParser != null) {
+ chartDesc = String.format("%1$s / %2$s",
+ logParser.getTagMap().get(mValueDescriptors.get(0).eventTag),
+ mValueDescriptors.get(0).valueName);
+ }
+ break;
+ }
+
+ if (chartDesc != null) {
+ return String.format("%1$s - %2$s", mName, chartDesc);
+ }
+ }
+
+ return mName;
+ }
+} \ No newline at end of file
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java
new file mode 100644
index 0000000..26296f3
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TableHelper;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+public class DisplayLog extends EventDisplay {
+ public DisplayLog(String name) {
+ super(name);
+ }
+
+ private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$
+ private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$
+ private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$
+ private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$
+ private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$
+ private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ mLogTable.removeAll();
+ }
+
+ /**
+ * Adds event to the display.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ addToLog(event, logParser);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) {
+ return createLogUI(parent, listener);
+ }
+
+ /**
+ * Adds an {@link EventContainer} to the log.
+ *
+ * @param event the event.
+ * @param logParser the log parser.
+ */
+ private void addToLog(EventContainer event, EventLogParser logParser) {
+ ScrollBar bar = mLogTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // get the date.
+ Calendar c = Calendar.getInstance();
+ long msec = (long) event.sec * 1000L;
+ c.setTimeInMillis(msec);
+
+ // convert the time into a string
+ String date = String.format("%1$tF %1$tT", c);
+
+ String eventName = logParser.getTagMap().get(event.mTag);
+ String pidName = Integer.toString(event.pid);
+
+ // get the value description
+ EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag);
+ if (valueDescription != null) {
+ for (int i = 0; i < valueDescription.length; i++) {
+ EventValueDescription description = valueDescription[i];
+ try {
+ String value = event.getValueAsString(i);
+
+ logValue(date, pidName, eventName, description.getName(), value,
+ description.getEventValueType(), description.getValueType());
+ } catch (InvalidTypeException e) {
+ logValue(date, pidName, eventName, description.getName(), e.getMessage(),
+ description.getEventValueType(), description.getValueType());
+ }
+ }
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ int itemCount = mLogTable.getItemCount();
+ if (itemCount > 0) {
+ mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by
+ * the list of descriptors. If an event is configured to be displayed by value and occurrence,
+ * only the values are displayed (as they mark an event occurrence anyway).
+ * <p/>This method is only called when at least one of the descriptor list is non empty.
+ *
+ * @param event
+ * @param logParser
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ */
+ protected void addToLog(EventContainer event, EventLogParser logParser,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+ ScrollBar bar = mLogTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // get the date.
+ Calendar c = Calendar.getInstance();
+ long msec = (long) event.sec * 1000L;
+ c.setTimeInMillis(msec);
+
+ // convert the time into a string
+ String date = String.format("%1$tF %1$tT", c);
+
+ String eventName = logParser.getTagMap().get(event.mTag);
+ String pidName = Integer.toString(event.pid);
+
+ if (valueDescriptors.size() > 0) {
+ for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+ logDescriptor(event, descriptor, date, pidName, eventName, logParser);
+ }
+ } else {
+ // we display the event. Since the StringBuilder contains the header (date, event name,
+ // pid) at this point, there isn't anything else to display.
+ }
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ int itemCount = mLogTable.getItemCount();
+ if (itemCount > 0) {
+ mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+ }
+ }
+ }
+
+
+ /**
+ * Logs a value in the ui.
+ *
+ * @param date
+ * @param pid
+ * @param event
+ * @param valueName
+ * @param value
+ * @param eventValueType
+ * @param valueType
+ */
+ private void logValue(String date, String pid, String event, String valueName,
+ String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) {
+
+ TableItem item = new TableItem(mLogTable, SWT.NONE);
+ item.setText(0, date);
+ item.setText(1, pid);
+ item.setText(2, event);
+ item.setText(3, valueName);
+ item.setText(4, value);
+
+ String type;
+ if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) {
+ type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString());
+ } else {
+ type = eventValueType.toString();
+ }
+
+ item.setText(5, type);
+ }
+
+ /**
+ * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}.
+ *
+ * @param event the EventContainer
+ * @param descriptor the ValueDisplayDescriptor defining which value to display.
+ * @param date the date of the event in a string.
+ * @param pidName
+ * @param eventName
+ * @param logParser
+ */
+ private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor,
+ String date, String pidName, String eventName, EventLogParser logParser) {
+
+ String value;
+ try {
+ value = event.getValueAsString(descriptor.valueIndex);
+ } catch (InvalidTypeException e) {
+ value = e.getMessage();
+ }
+
+ EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag);
+
+ EventValueDescription valueDescription = values[descriptor.valueIndex];
+
+ logValue(date, pidName, eventName, descriptor.valueName, value,
+ valueDescription.getEventValueType(), valueDescription.getValueType());
+ }
+
+ /**
+ * Creates the UI for a log display.
+ *
+ * @param parent the parent {@link Composite}
+ * @param listener the {@link ILogColumnListener} to notify on column resize events.
+ * @return the top Composite of the UI.
+ */
+ private Control createLogUI(Composite parent, final ILogColumnListener listener) {
+ Composite mainComp = new Composite(parent, SWT.NONE);
+ GridLayout gl;
+ mainComp.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ mainComp.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ mLogTable = null;
+ }
+ });
+
+ Label l = new Label(mainComp, SWT.CENTER);
+ l.setText(mName);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL |
+ SWT.BORDER);
+ mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ TableColumn col = TableHelper.createTableColumn(
+ mLogTable, "Time",
+ SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(0, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "pid",
+ SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(1, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Event",
+ SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(2, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Name",
+ SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(3, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Value",
+ SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(4, (TableColumn) source);
+ }
+ }
+ });
+
+ col = TableHelper.createTableColumn(
+ mLogTable, "Type",
+ SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$
+ col.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Object source = e.getSource();
+ if (source instanceof TableColumn) {
+ listener.columnResized(5, (TableColumn) source);
+ }
+ }
+ });
+
+ mLogTable.setHeaderVisible(true);
+ mLogTable.setLinesVisible(true);
+
+ return mainComp;
+ }
+
+ /**
+ * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+ * <p/>
+ * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+ * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+ * column passed as argument.
+ *
+ * @param index the index of the column to resize
+ * @param sourceColumn the original column that was resize, and on which we need to sync the
+ * index-th column width.
+ */
+ @Override
+ void resizeColumn(int index, TableColumn sourceColumn) {
+ if (mLogTable != null) {
+ TableColumn col = mLogTable.getColumn(index);
+ if (col != sourceColumn) {
+ col.setWidth(sourceColumn.getWidth());
+ }
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_LOG_ALL;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java
new file mode 100644
index 0000000..82cc7a4
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.FixedMillisecond;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.util.ShapeUtilities;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public class DisplaySync extends SyncCommon {
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasetsSync[];
+ private List<String> mTooltipsSync[];
+ private CustomXYToolTipGenerator mTooltipGenerators[];
+ private TimeSeries mDatasetsSyncTickle[];
+
+ // Dataset of error events to graph
+ private TimeSeries mDatasetError;
+
+ public DisplaySync(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Status");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+
+ XYBarRenderer br = new XYBarRenderer();
+ mDatasetsSync = new TimePeriodValues[NUM_AUTHS];
+ mTooltipsSync = new List[NUM_AUTHS];
+ mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS];
+
+ TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(0, br);
+
+ XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer();
+ ls.setBaseLinesVisible(false);
+ mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS];
+ TimeSeriesCollection tsc = new TimeSeriesCollection();
+ xyPlot.setDataset(1, tsc);
+ xyPlot.setRenderer(1, ls);
+
+ mDatasetError = new TimeSeries("Errors", FixedMillisecond.class);
+ xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError));
+ XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer();
+ errls.setBaseLinesVisible(false);
+ errls.setSeriesPaint(0, Color.RED);
+ xyPlot.setRenderer(2, errls);
+
+ for (int i = 0; i < NUM_AUTHS; i++) {
+ br.setSeriesPaint(i, AUTH_COLORS[i]);
+ ls.setSeriesPaint(i, AUTH_COLORS[i]);
+ mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]);
+ tpvc.addSeries(mDatasetsSync[i]);
+ mTooltipsSync[i] = new ArrayList<String>();
+ mTooltipGenerators[i] = new CustomXYToolTipGenerator();
+ br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]);
+ mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]);
+
+ mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle",
+ FixedMillisecond.class);
+ tsc.addSeries(mDatasetsSyncTickle[i]);
+ ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f));
+ }
+ }
+
+ /**
+ * Updates the display with a new event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ super.newEvent(event, logParser); // Handle sync operation
+ try {
+ if (event.mTag == EVENT_TICKLE) {
+ int auth = getAuth(event.getValueAsString(0));
+ if (auth >= 0) {
+ long msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1);
+ }
+ }
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Generate the height for an event.
+ * Height is somewhat arbitrarily the count of "things" that happened
+ * during the sync.
+ * When network traffic measurements are available, code should be modified
+ * to use that instead.
+ * @param details The details string associated with the event
+ * @return The height in arbirary units (0-100)
+ */
+ private int getHeightFromDetails(String details) {
+ if (details == null) {
+ return 1; // Arbitrary
+ }
+ int total = 0;
+ String parts[] = details.split("[a-zA-Z]");
+ for (String part : parts) {
+ if ("".equals(part)) continue;
+ total += Integer.parseInt(part);
+ }
+ if (total == 0) {
+ total = 1;
+ }
+ return total;
+ }
+
+ /**
+ * Generates the tooltips text for an event.
+ * This method decodes the cryptic details string.
+ * @param auth The authority associated with the event
+ * @param details The details string
+ * @param eventSource server, poll, etc.
+ * @return The text to display in the tooltips
+ */
+ private String getTextFromDetails(int auth, String details, int eventSource) {
+
+ StringBuffer sb = new StringBuffer();
+ sb.append(AUTH_NAMES[auth]).append(": \n");
+
+ Scanner scanner = new Scanner(details);
+ Pattern charPat = Pattern.compile("[a-zA-Z]");
+ Pattern numPat = Pattern.compile("[0-9]+");
+ while (scanner.hasNext()) {
+ String key = scanner.findInLine(charPat);
+ int val = Integer.parseInt(scanner.findInLine(numPat));
+ if (auth == GMAIL && "M".equals(key)) {
+ sb.append("messages from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "L".equals(key)) {
+ sb.append("labels from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "C".equals(key)) {
+ sb.append("check conversation requests from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "A".equals(key)) {
+ sb.append("attachments from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "U".equals(key)) {
+ sb.append("op updates from server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "u".equals(key)) {
+ sb.append("op updates to server: ").append(val).append("\n");
+ } else if (auth == GMAIL && "S".equals(key)) {
+ sb.append("send/receive cycles: ").append(val).append("\n");
+ } else if ("Q".equals(key)) {
+ sb.append("queries to server: ").append(val).append("\n");
+ } else if ("E".equals(key)) {
+ sb.append("entries from server: ").append(val).append("\n");
+ } else if ("u".equals(key)) {
+ sb.append("updates from client: ").append(val).append("\n");
+ } else if ("i".equals(key)) {
+ sb.append("inserts from client: ").append(val).append("\n");
+ } else if ("d".equals(key)) {
+ sb.append("deletes from client: ").append(val).append("\n");
+ } else if ("f".equals(key)) {
+ sb.append("full sync requested\n");
+ } else if ("r".equals(key)) {
+ sb.append("partial sync unavailable\n");
+ } else if ("X".equals(key)) {
+ sb.append("hard error\n");
+ } else if ("e".equals(key)) {
+ sb.append("number of parse exceptions: ").append(val).append("\n");
+ } else if ("c".equals(key)) {
+ sb.append("number of conflicts: ").append(val).append("\n");
+ } else if ("a".equals(key)) {
+ sb.append("number of auth exceptions: ").append(val).append("\n");
+ } else if ("D".equals(key)) {
+ sb.append("too many deletions\n");
+ } else if ("R".equals(key)) {
+ sb.append("too many retries: ").append(val).append("\n");
+ } else if ("b".equals(key)) {
+ sb.append("database error\n");
+ } else if ("x".equals(key)) {
+ sb.append("soft error\n");
+ } else if ("l".equals(key)) {
+ sb.append("sync already in progress\n");
+ } else if ("I".equals(key)) {
+ sb.append("io exception\n");
+ } else if (auth == CONTACTS && "p".equals(key)) {
+ sb.append("photos uploaded from client: ").append(val).append("\n");
+ } else if (auth == CONTACTS && "P".equals(key)) {
+ sb.append("photos downloaded from server: ").append(val).append("\n");
+ } else if (auth == CALENDAR && "F".equals(key)) {
+ sb.append("server refresh\n");
+ } else if (auth == CALENDAR && "s".equals(key)) {
+ sb.append("server diffs fetched\n");
+ } else {
+ sb.append(key).append("=").append(val);
+ }
+ }
+ if (eventSource == 0) {
+ sb.append("(server)");
+ } else if (eventSource == 1) {
+ sb.append("(local)");
+ } else if (eventSource == 2) {
+ sb.append("(poll)");
+ } else if (eventSource == 3) {
+ sb.append("(user)");
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Callback to process a sync event.
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (!newEvent) {
+ // Details arrived for a previous sync event
+ // Remove event before reinserting.
+ int lastItem = mDatasetsSync[auth].getItemCount();
+ mDatasetsSync[auth].delete(lastItem-1, lastItem-1);
+ mTooltipsSync[auth].remove(lastItem-1);
+ }
+ double height = getHeightFromDetails(details);
+ height = height / (stopTime - startTime + 1) * 10000;
+ if (height > 30) {
+ height = 30;
+ }
+ mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height);
+ mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource));
+ mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]);
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ long msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+ mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC;
+ }
+} \ No newline at end of file
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
new file mode 100644
index 0000000..36d90ce
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.RegularTimePeriod;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class DisplaySyncHistogram extends SyncCommon {
+
+ Map<SimpleTimePeriod, Integer> mTimePeriodMap[];
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasetsSyncHist[];
+
+ public DisplaySyncHistogram(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Histogram");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+
+ AbstractXYItemRenderer br = new XYBarRenderer();
+ mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1];
+ mTimePeriodMap = new HashMap[NUM_AUTHS + 1];
+
+ TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(br);
+
+ for (int i = 0; i < NUM_AUTHS + 1; i++) {
+ br.setSeriesPaint(i, AUTH_COLORS[i]);
+ mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]);
+ tpvc.addSeries(mDatasetsSyncHist[i]);
+ mTimePeriodMap[i] = new HashMap<SimpleTimePeriod, Integer>();
+
+ }
+ }
+
+ /**
+ * Callback to process a sync event.
+ *
+ * @param event The sync event
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (newEvent) {
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ auth = ERRORS;
+ }
+ double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+ addHistEvent(0, auth, delta);
+ } else {
+ // sync_details arrived for an event that has already been graphed.
+ if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+ // Item turns out to be in error, so transfer time from old auth to error.
+ double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+ addHistEvent(0, auth, -delta);
+ addHistEvent(0, ERRORS, delta);
+ }
+ }
+ }
+
+ /**
+ * Helper to add an event to the data series.
+ * Also updates error series if appropriate (x or X in details).
+ * @param stopTime Time event ends
+ * @param auth Sync authority
+ * @param value Value to graph for event
+ */
+ private void addHistEvent(long stopTime, int auth, double value) {
+ SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth);
+
+ // Loop over all datasets to do the stacking.
+ for (int i = auth; i <= ERRORS; i++) {
+ addToPeriod(mDatasetsSyncHist, i, hour, value);
+ }
+ }
+
+ private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period,
+ double value) {
+ int index;
+ if (mTimePeriodMap[auth].containsKey(period)) {
+ index = mTimePeriodMap[auth].get(period);
+ double oldValue = tpv[auth].getValue(index).doubleValue();
+ tpv[auth].update(index, oldValue + value);
+ } else {
+ index = tpv[auth].getItemCount();
+ mTimePeriodMap[auth].put(period, index);
+ tpv[auth].add(period, value);
+ }
+ }
+
+ /**
+ * Creates a multiple-hour time period for the histogram.
+ * @param time Time in milliseconds.
+ * @param numHoursWide: should divide into a day.
+ * @return SimpleTimePeriod covering the number of hours and containing time.
+ */
+ private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) {
+ Date date = new Date(time);
+ TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE;
+ Calendar calendar = Calendar.getInstance(zone);
+ calendar.setTime(date);
+ long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) +
+ calendar.get(Calendar.DAY_OF_YEAR) * 24;
+ int year = calendar.get(Calendar.YEAR);
+ hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide;
+ calendar.clear();
+ calendar.set(year, 0, 1, 0, 0); // Jan 1
+ long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000;
+ return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000);
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC_HIST;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java
new file mode 100644
index 0000000..9ce7045
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DisplaySyncPerf extends SyncCommon {
+
+ CustomXYToolTipGenerator mTooltipGenerator;
+ List mTooltips[];
+
+ // The series number for each graphed item.
+ // sync authorities are 0-3
+ private static final int DB_QUERY = 4;
+ private static final int DB_WRITE = 5;
+ private static final int HTTP_NETWORK = 6;
+ private static final int HTTP_PROCESSING = 7;
+ private static final int NUM_SERIES = (HTTP_PROCESSING + 1);
+ private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+ "DB Query", "DB Write", "HTTP Response", "HTTP Processing",};
+ private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+ Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY};
+ private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2};
+
+ // Values from data/etc/event-log-tags
+ private static final int EVENT_DB_OPERATION = 52000;
+ private static final int EVENT_HTTP_STATS = 52001;
+ // op types for EVENT_DB_OPERATION
+ final int EVENT_DB_QUERY = 0;
+ final int EVENT_DB_WRITE = 1;
+
+ // Information to graph for each authority
+ private TimePeriodValues mDatasets[];
+
+ /**
+ * TimePeriodValuesCollection that supports Y intervals. This allows the
+ * creation of "floating" bars, rather than bars rooted to the axis.
+ */
+ class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection {
+ /** default serial UID */
+ private static final long serialVersionUID = 1L;
+
+ private double yheight;
+
+ /**
+ * Constructs a collection of bars with a fixed Y height.
+ *
+ * @param yheight The height of the bars.
+ */
+ YIntervalTimePeriodValuesCollection(double yheight) {
+ this.yheight = yheight;
+ }
+
+ /**
+ * Returns ending Y value that is a fixed amount greater than the starting value.
+ *
+ * @param series the series (zero-based index).
+ * @param item the item (zero-based index).
+ * @return The ending Y value for the specified series and item.
+ */
+ @Override
+ public Number getEndY(int series, int item) {
+ return getY(series, item).doubleValue() + yheight;
+ }
+ }
+
+ /**
+ * Constructs a graph of network and database stats.
+ *
+ * @param name The name of this graph in the graph list.
+ */
+ public DisplaySyncPerf(String name) {
+ super(name);
+ }
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ @Override
+ public Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener) {
+ Control composite = createCompositeChart(parent, logParser, "Sync Performance");
+ resetUI();
+ return composite;
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ super.resetUI();
+ XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.getRangeAxis().setVisible(false);
+ mTooltipGenerator = new CustomXYToolTipGenerator();
+ mTooltips = new List[NUM_SERIES];
+
+ XYBarRenderer br = new XYBarRenderer();
+ br.setUseYInterval(true);
+ mDatasets = new TimePeriodValues[NUM_SERIES];
+
+ TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1);
+ xyPlot.setDataset(tpvc);
+ xyPlot.setRenderer(br);
+
+ for (int i = 0; i < NUM_SERIES; i++) {
+ br.setSeriesPaint(i, SERIES_COLORS[i]);
+ mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]);
+ tpvc.addSeries(mDatasets[i]);
+ mTooltips[i] = new ArrayList<String>();
+ mTooltipGenerator.addToolTipSeries(mTooltips[i]);
+ br.setSeriesToolTipGenerator(i, mTooltipGenerator);
+ }
+ }
+
+ /**
+ * Updates the display with a new event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ super.newEvent(event, logParser); // Handle sync operation
+ try {
+ if (event.mTag == EVENT_DB_OPERATION) {
+ // 52000 db_operation (name|3),(op_type|1|5),(time|2|3)
+ String tip = event.getValueAsString(0);
+ long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ int opType = Integer.parseInt(event.getValueAsString(1));
+ long duration = Long.parseLong(event.getValueAsString(2));
+
+ if (opType == EVENT_DB_QUERY) {
+ mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime),
+ SERIES_YCOORD[DB_QUERY]);
+ mTooltips[DB_QUERY].add(tip);
+ } else if (opType == EVENT_DB_WRITE) {
+ mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime),
+ SERIES_YCOORD[DB_WRITE]);
+ mTooltips[DB_WRITE].add(tip);
+ }
+ } else if (event.mTag == EVENT_HTTP_STATS) {
+ // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2)
+ String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) +
+ ", rx: " + event.getValueAsString(4);
+ long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ long netEndTime = endTime - Long.parseLong(event.getValueAsString(2));
+ long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1));
+ mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime),
+ SERIES_YCOORD[HTTP_NETWORK]);
+ mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime),
+ SERIES_YCOORD[HTTP_PROCESSING]);
+ mTooltips[HTTP_NETWORK].add(tip);
+ mTooltips[HTTP_PROCESSING].add(tip);
+ }
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Callback from super.newEvent to process a sync event.
+ *
+ * @param event The sync event
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource
+ */
+ @Override
+ void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource) {
+ if (newEvent) {
+ mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]);
+ }
+ }
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ @Override
+ int getDisplayType() {
+ return DISPLAY_TYPE_SYNC_PERF;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java
new file mode 100644
index 0000000..2223a4d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java
@@ -0,0 +1,971 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.event.ChartChangeEvent;
+import org.jfree.chart.event.ChartChangeEventType;
+import org.jfree.chart.event.ChartChangeListener;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a custom display of one or more events.
+ */
+abstract class EventDisplay {
+
+ private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$
+ private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$
+ private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$
+ private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$
+
+ private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$
+
+ public final static int DISPLAY_TYPE_LOG_ALL = 0;
+ public final static int DISPLAY_TYPE_FILTERED_LOG = 1;
+ public final static int DISPLAY_TYPE_GRAPH = 2;
+ public final static int DISPLAY_TYPE_SYNC = 3;
+ public final static int DISPLAY_TYPE_SYNC_HIST = 4;
+ public final static int DISPLAY_TYPE_SYNC_PERF = 5;
+
+ private final static int EVENT_CHECK_FAILED = 0;
+ protected final static int EVENT_CHECK_SAME_TAG = 1;
+ protected final static int EVENT_CHECK_SAME_VALUE = 2;
+
+ /**
+ * Creates the appropriate EventDisplay subclass.
+ *
+ * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc)
+ * @param name the name of the display
+ * @return the created object
+ */
+ public static EventDisplay eventDisplayFactory(int type, String name) {
+ switch (type) {
+ case DISPLAY_TYPE_LOG_ALL:
+ return new DisplayLog(name);
+ case DISPLAY_TYPE_FILTERED_LOG:
+ return new DisplayFilteredLog(name);
+ case DISPLAY_TYPE_SYNC:
+ return new DisplaySync(name);
+ case DISPLAY_TYPE_SYNC_HIST:
+ return new DisplaySyncHistogram(name);
+ case DISPLAY_TYPE_GRAPH:
+ return new DisplayGraph(name);
+ case DISPLAY_TYPE_SYNC_PERF:
+ return new DisplaySyncPerf(name);
+ default:
+ throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Adds event to the display.
+ * @param event The event
+ * @param logParser The log parser.
+ */
+ abstract void newEvent(EventContainer event, EventLogParser logParser);
+
+ /**
+ * Resets the display.
+ */
+ abstract void resetUI();
+
+ /**
+ * Gets display type
+ *
+ * @return display type as an integer
+ */
+ abstract int getDisplayType();
+
+ /**
+ * Creates the UI for the event display.
+ *
+ * @param parent the parent composite.
+ * @param logParser the current log parser.
+ * @return the created control (which may have children).
+ */
+ abstract Control createComposite(final Composite parent, EventLogParser logParser,
+ final ILogColumnListener listener);
+
+ interface ILogColumnListener {
+ void columnResized(int index, TableColumn sourceColumn);
+ }
+
+ /**
+ * Describes an event to be displayed.
+ */
+ static class OccurrenceDisplayDescriptor {
+
+ int eventTag = -1;
+ int seriesValueIndex = -1;
+ boolean includePid = false;
+ int filterValueIndex = -1;
+ CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO;
+ Object filterValue = null;
+
+ OccurrenceDisplayDescriptor() {
+ }
+
+ OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) {
+ replaceWith(descriptor);
+ }
+
+ OccurrenceDisplayDescriptor(int eventTag) {
+ this.eventTag = eventTag;
+ }
+
+ OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) {
+ this.eventTag = eventTag;
+ this.seriesValueIndex = seriesValueIndex;
+ }
+
+ void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+ eventTag = descriptor.eventTag;
+ seriesValueIndex = descriptor.seriesValueIndex;
+ includePid = descriptor.includePid;
+ filterValueIndex = descriptor.filterValueIndex;
+ filterCompareMethod = descriptor.filterCompareMethod;
+ filterValue = descriptor.filterValue;
+ }
+
+ /**
+ * Loads the descriptor parameter from a storage string. The storage string must have
+ * been generated with {@link #getStorageString()}.
+ *
+ * @param storageString the storage string
+ */
+ final void loadFrom(String storageString) {
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR));
+ loadFrom(values, 0);
+ }
+
+ /**
+ * Loads the parameters from an array of strings.
+ *
+ * @param storageStrings the strings representing each parameter.
+ * @param index the starting index in the array of strings.
+ * @return the new index in the array.
+ */
+ protected int loadFrom(String[] storageStrings, int index) {
+ eventTag = Integer.parseInt(storageStrings[index++]);
+ seriesValueIndex = Integer.parseInt(storageStrings[index++]);
+ includePid = Boolean.parseBoolean(storageStrings[index++]);
+ filterValueIndex = Integer.parseInt(storageStrings[index++]);
+ try {
+ filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]);
+ } catch (IllegalArgumentException e) {
+ // if the name does not match any known CompareMethod, we init it to the default one
+ filterCompareMethod = CompareMethod.EQUAL_TO;
+ }
+ String value = storageStrings[index++];
+ if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) {
+ filterValue = EventValueType.getObjectFromStorageString(value);
+ }
+
+ return index;
+ }
+
+ /**
+ * Returns the storage string for the receiver.
+ */
+ String getStorageString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(eventTag);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(seriesValueIndex);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(Boolean.toString(includePid));
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(filterValueIndex);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(filterCompareMethod.name());
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ if (filterValue != null) {
+ String value = EventValueType.getStorageString(filterValue);
+ if (value != null) {
+ sb.append(value);
+ } else {
+ sb.append(FILTER_VALUE_NULL);
+ }
+ } else {
+ sb.append(FILTER_VALUE_NULL);
+ }
+
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Describes an event value to be displayed.
+ */
+ static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor {
+ String valueName;
+ int valueIndex = -1;
+
+ ValueDisplayDescriptor() {
+ super();
+ }
+
+ ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) {
+ super();
+ replaceWith(descriptor);
+ }
+
+ ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) {
+ super(eventTag);
+ this.valueName = valueName;
+ this.valueIndex = valueIndex;
+ }
+
+ ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex,
+ int seriesValueIndex) {
+ super(eventTag, seriesValueIndex);
+ this.valueName = valueName;
+ this.valueIndex = valueIndex;
+ }
+
+ @Override
+ void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+ super.replaceWith(descriptor);
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor;
+ valueName = valueDescriptor.valueName;
+ valueIndex = valueDescriptor.valueIndex;
+ }
+ }
+
+ /**
+ * Loads the parameters from an array of strings.
+ *
+ * @param storageStrings the strings representing each parameter.
+ * @param index the starting index in the array of strings.
+ * @return the new index in the array.
+ */
+ @Override
+ protected int loadFrom(String[] storageStrings, int index) {
+ index = super.loadFrom(storageStrings, index);
+ valueName = storageStrings[index++];
+ valueIndex = Integer.parseInt(storageStrings[index++]);
+ return index;
+ }
+
+ /**
+ * Returns the storage string for the receiver.
+ */
+ @Override
+ String getStorageString() {
+ String superStorage = super.getStorageString();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(superStorage);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(valueName);
+ sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+ sb.append(valueIndex);
+
+ return sb.toString();
+ }
+ }
+
+ /* ==================
+ * Event Display parameters.
+ * ================== */
+ protected String mName;
+
+ private boolean mPidFiltering = false;
+
+ private ArrayList<Integer> mPidFilterList = null;
+
+ protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors =
+ new ArrayList<ValueDisplayDescriptor>();
+ private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors =
+ new ArrayList<OccurrenceDisplayDescriptor>();
+
+ /* ==================
+ * Event Display members for display purpose.
+ * ================== */
+ // chart objects
+ /**
+ * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+ */
+ protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap =
+ new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+ /**
+ * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+ */
+ protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap =
+ new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+
+ /**
+ * This is a map of (ValueType, dataset)
+ */
+ protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap =
+ new HashMap<ValueType, TimeSeriesCollection>();
+
+ protected JFreeChart mChart;
+ protected TimeSeriesCollection mOccurrenceDataSet;
+ protected int mDataSetCount;
+ private ChartComposite mChartComposite;
+ protected long mMaximumChartItemAge = -1;
+ protected long mHistWidth = 1;
+
+ // log objects.
+ protected Table mLogTable;
+
+ /* ==================
+ * Misc data.
+ * ================== */
+ protected int mValueDescriptorCheck = EVENT_CHECK_FAILED;
+
+ EventDisplay(String name) {
+ mName = name;
+ }
+
+ static EventDisplay clone(EventDisplay from) {
+ EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName());
+ ed.mName = from.mName;
+ ed.mPidFiltering = from.mPidFiltering;
+ ed.mMaximumChartItemAge = from.mMaximumChartItemAge;
+ ed.mHistWidth = from.mHistWidth;
+
+ if (from.mPidFilterList != null) {
+ ed.mPidFilterList = new ArrayList<Integer>();
+ ed.mPidFilterList.addAll(from.mPidFilterList);
+ }
+
+ for (ValueDisplayDescriptor desc : from.mValueDescriptors) {
+ ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc));
+ }
+ ed.mValueDescriptorCheck = from.mValueDescriptorCheck;
+
+ for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) {
+ ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc));
+ }
+ return ed;
+ }
+
+ /**
+ * Returns the parameters of the receiver as a single String for storage.
+ */
+ String getStorageString() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(mName);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDisplayType());
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(Boolean.toString(mPidFiltering));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getPidStorageString());
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDescriptorStorageString(mValueDescriptors));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(getDescriptorStorageString(mOccurrenceDescriptors));
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(mMaximumChartItemAge);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+ sb.append(mHistWidth);
+ sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+
+ return sb.toString();
+ }
+
+ void setName(String name) {
+ mName = name;
+ }
+
+ String getName() {
+ return mName;
+ }
+
+ void setPidFiltering(boolean filterByPid) {
+ mPidFiltering = filterByPid;
+ }
+
+ boolean getPidFiltering() {
+ return mPidFiltering;
+ }
+
+ void setPidFilterList(ArrayList<Integer> pids) {
+ if (mPidFiltering == false) {
+ new InvalidParameterException();
+ }
+
+ mPidFilterList = pids;
+ }
+
+ ArrayList<Integer> getPidFilterList() {
+ return mPidFilterList;
+ }
+
+ void addPidFiler(int pid) {
+ if (mPidFiltering == false) {
+ new InvalidParameterException();
+ }
+
+ if (mPidFilterList == null) {
+ mPidFilterList = new ArrayList<Integer>();
+ }
+
+ mPidFilterList.add(pid);
+ }
+
+ /**
+ * Returns an iterator to the list of {@link ValueDisplayDescriptor}.
+ */
+ Iterator<ValueDisplayDescriptor> getValueDescriptors() {
+ return mValueDescriptors.iterator();
+ }
+
+ /**
+ * Update checks on the descriptors. Must be called whenever a descriptor is modified outside
+ * of this class.
+ */
+ void updateValueDescriptorCheck() {
+ mValueDescriptorCheck = checkDescriptors();
+ }
+
+ /**
+ * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}.
+ */
+ Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() {
+ return mOccurrenceDescriptors.iterator();
+ }
+
+ /**
+ * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a
+ * {@link ValueDisplayDescriptor}.
+ *
+ * @param descriptor the descriptor to be added.
+ */
+ void addDescriptor(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ mValueDescriptors.add((ValueDisplayDescriptor) descriptor);
+ mValueDescriptorCheck = checkDescriptors();
+ } else {
+ mOccurrenceDescriptors.add(descriptor);
+ }
+ }
+
+ /**
+ * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}).
+ *
+ * @param descriptorClass the class of the descriptor to return.
+ * @param index the index of the descriptor to return.
+ * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor}
+ * or <code>null</code> if <code>descriptorClass</code> is another class.
+ */
+ OccurrenceDisplayDescriptor getDescriptor(
+ Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+
+ if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+ return mOccurrenceDescriptors.get(index);
+ } else if (descriptorClass == ValueDisplayDescriptor.class) {
+ return mValueDescriptors.get(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes a descriptor based on its class and index.
+ *
+ * @param descriptorClass the class of the descriptor.
+ * @param index the index of the descriptor to be removed.
+ */
+ void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+ if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+ mOccurrenceDescriptors.remove(index);
+ } else if (descriptorClass == ValueDisplayDescriptor.class) {
+ mValueDescriptors.remove(index);
+ mValueDescriptorCheck = checkDescriptors();
+ }
+ }
+
+ Control createCompositeChart(final Composite parent, EventLogParser logParser,
+ String title) {
+ mChart = ChartFactory.createTimeSeriesChart(
+ null,
+ null /* timeAxisLabel */,
+ null /* valueAxisLabel */,
+ null, /* dataset. set below */
+ true /* legend */,
+ false /* tooltips */,
+ false /* urls */);
+
+ // get the font to make a proper title. We need to convert the swt font,
+ // into an awt font.
+ Font f = parent.getFont();
+ FontData[] fData = f.getFontData();
+
+ // event though on Mac OS there could be more than one fontData, we'll only use
+ // the first one.
+ FontData firstFontData = fData[0];
+
+ java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(),
+ firstFontData, true /* ensureSameSize */);
+
+
+ mChart.setTitle(new TextTitle(title, awtFont));
+
+ final XYPlot xyPlot = mChart.getXYPlot();
+ xyPlot.setRangeCrosshairVisible(true);
+ xyPlot.setRangeCrosshairLockedOnData(true);
+ xyPlot.setDomainCrosshairVisible(true);
+ xyPlot.setDomainCrosshairLockedOnData(true);
+
+ mChart.addChangeListener(new ChartChangeListener() {
+ public void chartChanged(ChartChangeEvent event) {
+ ChartChangeEventType type = event.getType();
+ if (type == ChartChangeEventType.GENERAL) {
+ // because the value we need (rangeCrosshair and domainCrosshair) are
+ // updated on the draw, but the notification happens before the draw,
+ // we process the click in a future runnable!
+ parent.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ processClick(xyPlot);
+ }
+ });
+ }
+ }
+ });
+
+ mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart,
+ ChartComposite.DEFAULT_WIDTH,
+ ChartComposite.DEFAULT_HEIGHT,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+ ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+ 3000, // max draw width. We don't want it to zoom, so we put a big number
+ 3000, // max draw height. We don't want it to zoom, so we put a big number
+ true, // off-screen buffer
+ true, // properties
+ true, // save
+ true, // print
+ true, // zoom
+ true); // tooltips
+
+ mChartComposite.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ mValueTypeDataSetMap.clear();
+ mDataSetCount = 0;
+ mOccurrenceDataSet = null;
+ mChart = null;
+ mChartComposite = null;
+ mValueDescriptorSeriesMap.clear();
+ mOcurrenceDescriptorSeriesMap.clear();
+ }
+ });
+
+ return mChartComposite;
+
+ }
+
+ private void processClick(XYPlot xyPlot) {
+ double rangeValue = xyPlot.getRangeCrosshairValue();
+ if (rangeValue != 0) {
+ double domainValue = xyPlot.getDomainCrosshairValue();
+
+ Millisecond msec = new Millisecond(new Date((long) domainValue));
+
+ // look for values in the dataset that contains data at this TimePeriod
+ Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet();
+
+ for (ValueDisplayDescriptor descKey : descKeys) {
+ HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey);
+
+ Set<Integer> pidKeys = map.keySet();
+
+ for (Integer pidKey : pidKeys) {
+ TimeSeries series = map.get(pidKey);
+
+ Number value = series.getValue(msec);
+ if (value != null) {
+ // found a match. lets check against the actual value.
+ if (value.doubleValue() == rangeValue) {
+
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+ * Subclasses can override if necessary.
+ * <p/>
+ * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+ * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+ * column passed as argument.
+ *
+ * @param index the index of the column to resize
+ * @param sourceColumn the original column that was resize, and on which we need to sync the
+ * index-th column width.
+ */
+ void resizeColumn(int index, TableColumn sourceColumn) {
+ }
+
+ /**
+ * Sets the current {@link EventLogParser} object.
+ * Subclasses can override if necessary.
+ */
+ protected void setNewLogParser(EventLogParser logParser) {
+ }
+
+ /**
+ * Prepares the {@link EventDisplay} for a multi event display.
+ */
+ void startMultiEventDisplay() {
+ if (mLogTable != null) {
+ mLogTable.setRedraw(false);
+ }
+ }
+
+ /**
+ * Finalizes the {@link EventDisplay} after a multi event display.
+ */
+ void endMultiEventDisplay() {
+ if (mLogTable != null) {
+ mLogTable.setRedraw(true);
+ }
+ }
+
+ /**
+ * Returns the {@link Table} object used to display events, if any.
+ *
+ * @return a Table object or <code>null</code>.
+ */
+ Table getTable() {
+ return mLogTable;
+ }
+
+ /**
+ * Loads a new {@link EventDisplay} from a storage string. The string must have been created
+ * with {@link #getStorageString()}.
+ *
+ * @param storageString the storage string
+ * @return a new {@link EventDisplay} or null if the load failed.
+ */
+ static EventDisplay load(String storageString) {
+ if (storageString.length() > 0) {
+ // the storage string is separated by ':'
+ String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR));
+
+ try {
+ int index = 0;
+
+ String name = values[index++];
+ int displayType = Integer.parseInt(values[index++]);
+ boolean pidFiltering = Boolean.parseBoolean(values[index++]);
+
+ EventDisplay ed = eventDisplayFactory(displayType, name);
+ ed.setPidFiltering(pidFiltering);
+
+ // because empty sections are removed by String.split(), we have to check
+ // the index for those.
+ if (index < values.length) {
+ ed.loadPidFilters(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.loadValueDescriptors(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.loadOccurrenceDescriptors(values[index++]);
+ }
+
+ ed.updateValueDescriptorCheck();
+
+ if (index < values.length) {
+ ed.mMaximumChartItemAge = Long.parseLong(values[index++]);
+ }
+
+ if (index < values.length) {
+ ed.mHistWidth = Long.parseLong(values[index++]);
+ }
+
+ return ed;
+ } catch (RuntimeException re) {
+ // we'll return null below.
+ Log.e("ddms", re);
+ }
+ }
+
+ return null;
+ }
+
+ private String getPidStorageString() {
+ if (mPidFilterList != null) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (Integer i : mPidFilterList) {
+ if (first == false) {
+ sb.append(PID_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+ sb.append(i);
+ }
+
+ return sb.toString();
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+
+ private void loadPidFilters(String storageString) {
+ if (storageString.length() > 0) {
+ String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ if (mPidFilterList == null) {
+ mPidFilterList = new ArrayList<Integer>();
+ }
+ mPidFilterList.add(Integer.parseInt(value));
+ }
+ }
+ }
+
+ private String getDescriptorStorageString(
+ ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+
+ for (OccurrenceDisplayDescriptor descriptor : descriptorList) {
+ if (first == false) {
+ sb.append(DESCRIPTOR_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+ sb.append(descriptor.getStorageString());
+ }
+
+ return sb.toString();
+ }
+
+ private void loadOccurrenceDescriptors(String storageString) {
+ if (storageString.length() == 0) {
+ return;
+ }
+
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor();
+ desc.loadFrom(value);
+ mOccurrenceDescriptors.add(desc);
+ }
+ }
+
+ private void loadValueDescriptors(String storageString) {
+ if (storageString.length() == 0) {
+ return;
+ }
+
+ String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ ValueDisplayDescriptor desc = new ValueDisplayDescriptor();
+ desc.loadFrom(value);
+ mValueDescriptors.add(desc);
+ }
+ }
+
+ /**
+ * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another
+ * list if they are configured to display the {@link EventContainer}
+ *
+ * @param event the event container
+ * @param fullList the list with all the descriptors.
+ * @param outList the list to fill.
+ */
+ @SuppressWarnings("unchecked")
+ private void getDescriptors(EventContainer event,
+ ArrayList<? extends OccurrenceDisplayDescriptor> fullList,
+ ArrayList outList) {
+ for (OccurrenceDisplayDescriptor descriptor : fullList) {
+ try {
+ // first check the event tag.
+ if (descriptor.eventTag == event.mTag) {
+ // now check if we have a filter on a value
+ if (descriptor.filterValueIndex == -1 ||
+ event.testValue(descriptor.filterValueIndex, descriptor.filterValue,
+ descriptor.filterCompareMethod)) {
+ outList.add(descriptor);
+ }
+ }
+ } catch (InvalidTypeException ite) {
+ // if the filter for the descriptor was incorrect, we ignore the descriptor.
+ } catch (ArrayIndexOutOfBoundsException aioobe) {
+ // if the index was wrong (the event content may have changed since we setup the
+ // display), we do nothing but log the error
+ Log.e("Event Log", String.format(
+ "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$
+ descriptor.filterValueIndex, descriptor.eventTag));
+ }
+ }
+ }
+
+ /**
+ * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor}
+ * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event.
+ *
+ * @param event
+ * @param valueDescriptors
+ * @param occurrenceDescriptors
+ * @return true if the event should be displayed.
+ */
+
+ protected boolean filterEvent(EventContainer event,
+ ArrayList<ValueDisplayDescriptor> valueDescriptors,
+ ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+
+ // test the pid first (if needed)
+ if (mPidFiltering && mPidFilterList != null) {
+ boolean found = false;
+ for (int pid : mPidFilterList) {
+ if (pid == event.pid) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found == false) {
+ return false;
+ }
+ }
+
+ // now get the list of matching descriptors
+ getDescriptors(event, mValueDescriptors, valueDescriptors);
+ getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors);
+
+ // and return whether there is at least one match in either list.
+ return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0);
+ }
+
+ /**
+ * Checks all the {@link ValueDisplayDescriptor} for similarity.
+ * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG.
+ * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE
+ *
+ * @return flag as described above
+ */
+ private int checkDescriptors() {
+ if (mValueDescriptors.size() < 2) {
+ return EVENT_CHECK_SAME_VALUE;
+ }
+
+ int tag = -1;
+ int index = -1;
+ for (ValueDisplayDescriptor display : mValueDescriptors) {
+ if (tag == -1) {
+ tag = display.eventTag;
+ index = display.valueIndex;
+ } else {
+ if (tag != display.eventTag) {
+ return EVENT_CHECK_FAILED;
+ } else {
+ if (index != -1) {
+ if (index != display.valueIndex) {
+ index = -1;
+ }
+ }
+ }
+ }
+ }
+
+ if (index == -1) {
+ return EVENT_CHECK_SAME_TAG;
+ }
+
+ return EVENT_CHECK_SAME_VALUE;
+ }
+
+ /**
+ * Resets the time limit on the chart to be infinite.
+ */
+ void resetChartTimeLimit() {
+ mMaximumChartItemAge = -1;
+ }
+
+ /**
+ * Sets the time limit on the charts.
+ *
+ * @param timeLimit the time limit in seconds.
+ */
+ void setChartTimeLimit(long timeLimit) {
+ mMaximumChartItemAge = timeLimit;
+ }
+
+ long getChartTimeLimit() {
+ return mMaximumChartItemAge;
+ }
+
+ /**
+ * m
+ * Resets the histogram width
+ */
+ void resetHistWidth() {
+ mHistWidth = 1;
+ }
+
+ /**
+ * Sets the histogram width
+ *
+ * @param histWidth the width in hours
+ */
+ void setHistWidth(long histWidth) {
+ mHistWidth = histWidth;
+ }
+
+ long getHistWidth() {
+ return mHistWidth;
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java
new file mode 100644
index 0000000..88c3cb2
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java
@@ -0,0 +1,955 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.IImageLoader;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+
+class EventDisplayOptions extends Dialog {
+ private static final int DLG_WIDTH = 700;
+ private static final int DLG_HEIGHT = 700;
+
+ private IImageLoader mImageLoader;
+
+ private Shell mParent;
+ private Shell mShell;
+
+ private boolean mEditStatus = false;
+ private final ArrayList<EventDisplay> mDisplayList = new ArrayList<EventDisplay>();
+
+ /* LEFT LIST */
+ private List mEventDisplayList;
+ private Button mEventDisplayNewButton;
+ private Button mEventDisplayDeleteButton;
+ private Button mEventDisplayUpButton;
+ private Button mEventDisplayDownButton;
+ private Text mDisplayWidthText;
+ private Text mDisplayHeightText;
+
+ /* WIDGETS ON THE RIGHT */
+ private Text mDisplayNameText;
+ private Combo mDisplayTypeCombo;
+ private Group mChartOptions;
+ private Group mHistOptions;
+ private Button mPidFilterCheckBox;
+ private Text mPidText;
+
+ /** Map with (event-tag, event name) */
+ private Map<Integer, String> mEventTagMap;
+
+ /** Map with (event-tag, array of value info for the event) */
+ private Map<Integer, EventValueDescription[]> mEventDescriptionMap;
+
+ /** list of current pids */
+ private ArrayList<Integer> mPidList;
+
+ private EventLogParser mLogParser;
+
+ private Group mInfoGroup;
+
+ private static class SelectionWidgets {
+ private List mList;
+ private Button mNewButton;
+ private Button mEditButton;
+ private Button mDeleteButton;
+
+ private void setEnabled(boolean enable) {
+ mList.setEnabled(enable);
+ mNewButton.setEnabled(enable);
+ mEditButton.setEnabled(enable);
+ mDeleteButton.setEnabled(enable);
+ }
+ }
+
+ private SelectionWidgets mValueSelection;
+ private SelectionWidgets mOccurrenceSelection;
+
+ /** flag to temporarly disable processing of {@link Text} changes, so that
+ * {@link Text#setText(String)} can be called safely. */
+ private boolean mProcessTextChanges = true;
+ private Text mTimeLimitText;
+ private Text mHistWidthText;
+
+ EventDisplayOptions(IImageLoader imageLoader, Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ mImageLoader = imageLoader;
+ }
+
+ /**
+ * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the
+ * list.
+ * @param logParser
+ * @param displayList
+ * @param eventList
+ * @return true if the list of {@link EventDisplay} objects was updated.
+ */
+ boolean open(EventLogParser logParser, ArrayList<EventDisplay> displayList,
+ ArrayList<EventContainer> eventList) {
+ mLogParser = logParser;
+
+ if (logParser != null) {
+ // we need 2 things from the parser.
+ // the event tag / event name map
+ mEventTagMap = logParser.getTagMap();
+
+ // the event info map
+ mEventDescriptionMap = logParser.getEventInfoMap();
+ }
+
+ // make a copy of the EventDisplay list since we'll use working copies.
+ duplicateEventDisplay(displayList);
+
+ // build a list of pid from the list of events.
+ buildPidList(eventList);
+
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ // Set the dialog size.
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.layout();
+
+ // actually open the dialog
+ mShell.open();
+
+ // event loop until the dialog is closed.
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ return mEditStatus;
+ }
+
+ ArrayList<EventDisplay> getEventDisplays() {
+ return mDisplayList;
+ }
+
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Event Display Configuration");
+
+ mShell.setLayout(new GridLayout(1, true));
+
+ final Composite topPanel = new Composite(mShell, SWT.NONE);
+ topPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ topPanel.setLayout(new GridLayout(2, false));
+
+ // create the tree on the left and the controls on the right.
+ Composite leftPanel = new Composite(topPanel, SWT.NONE);
+ Composite rightPanel = new Composite(topPanel, SWT.NONE);
+
+ createLeftPanel(leftPanel);
+ createRightPanel(rightPanel);
+
+ mShell.addListener(SWT.Close, new Listener() {
+ public void handleEvent(Event event) {
+ event.doit = true;
+ }
+ });
+
+ Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Composite bottomButtons = new Composite(mShell, SWT.NONE);
+ bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout gl;
+ bottomButtons.setLayout(gl = new GridLayout(2, true));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Button okButton = new Button(bottomButtons, SWT.PUSH);
+ okButton.setText("OK");
+ okButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ Button cancelButton = new Button(bottomButtons, SWT.PUSH);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // cancel the modification flag.
+ mEditStatus = false;
+
+ // and close
+ mShell.close();
+ }
+ });
+
+ enable(false);
+
+ // fill the list with the current display
+ fillEventDisplayList();
+ }
+
+ private void createLeftPanel(Composite leftPanel) {
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ GridLayout gl;
+
+ leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+ leftPanel.setLayout(gl = new GridLayout(1, false));
+ gl.verticalSpacing = 1;
+
+ mEventDisplayList = new List(leftPanel,
+ SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION);
+ mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mEventDisplayList.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleEventDisplaySelection();
+ }
+ });
+
+ Composite bottomControls = new Composite(leftPanel, SWT.NONE);
+ bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ bottomControls.setLayout(gl = new GridLayout(5, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing = 0;
+ gl.horizontalSpacing = 0;
+
+ mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayNewButton.setImage(mImageLoader.loadImage("add.png", // $NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayNewButton.setToolTipText("Adds a new event display");
+ mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ createNewEventDisplay();
+ }
+ });
+
+ mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayDeleteButton.setImage(mImageLoader.loadImage("delete.png", // $NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display");
+ mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ deleteEventDisplay();
+ }
+ });
+
+ mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayUpButton.setImage(mImageLoader.loadImage("up.png", // $NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayUpButton.setToolTipText("Moves the selected event display up");
+ mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get current selection.
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection > 0) {
+ // update the list of EventDisplay.
+ EventDisplay display = mDisplayList.remove(selection);
+ mDisplayList.add(selection - 1, display);
+
+ // update the list widget
+ mEventDisplayList.remove(selection);
+ mEventDisplayList.add(display.getName(), selection - 1);
+
+ // update the selection and reset the ui.
+ mEventDisplayList.select(selection - 1);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+ }
+ });
+
+ mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+ mEventDisplayDownButton.setImage(mImageLoader.loadImage("down.png", // $NON-NLS-1$
+ leftPanel.getDisplay()));
+ mEventDisplayDownButton.setToolTipText("Moves the selected event display down");
+ mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get current selection.
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) {
+ // update the list of EventDisplay.
+ EventDisplay display = mDisplayList.remove(selection);
+ mDisplayList.add(selection + 1, display);
+
+ // update the list widget
+ mEventDisplayList.remove(selection);
+ mEventDisplayList.add(display.getName(), selection + 1);
+
+ // update the selection and reset the ui.
+ mEventDisplayList.select(selection + 1);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+ }
+ });
+
+ Group sizeGroup = new Group(leftPanel, SWT.NONE);
+ sizeGroup.setText("Display Size:");
+ sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ sizeGroup.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(sizeGroup, SWT.NONE);
+ l.setText("Width:");
+
+ mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+ mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayWidthText.setText(Integer.toString(
+ store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH)));
+ mDisplayWidthText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ String text = mDisplayWidthText.getText().trim();
+ try {
+ store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text));
+ setModified();
+ } catch (NumberFormatException nfe) {
+ // do something?
+ }
+ }
+ });
+
+ l = new Label(sizeGroup, SWT.NONE);
+ l.setText("Height:");
+
+ mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+ mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayHeightText.setText(Integer.toString(
+ store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT)));
+ mDisplayHeightText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ String text = mDisplayHeightText.getText().trim();
+ try {
+ store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text));
+ setModified();
+ } catch (NumberFormatException nfe) {
+ // do something?
+ }
+ }
+ });
+ }
+
+ private void createRightPanel(Composite rightPanel) {
+ rightPanel.setLayout(new GridLayout(1, true));
+ rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mInfoGroup = new Group(rightPanel, SWT.NONE);
+ mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mInfoGroup.setLayout(new GridLayout(2, false));
+
+ Label nameLabel = new Label(mInfoGroup, SWT.LEFT);
+ nameLabel.setText("Name:");
+
+ mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+ mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mDisplayNameText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ if (mProcessTextChanges) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ eventDisplay.setName(mDisplayNameText.getText());
+ int index = mEventDisplayList.getSelectionIndex();
+ mEventDisplayList.remove(index);
+ mEventDisplayList.add(eventDisplay.getName(), index);
+ mEventDisplayList.select(index);
+ handleEventDisplaySelection();
+ setModified();
+ }
+ }
+ }
+ });
+
+ Label displayLabel = new Label(mInfoGroup, SWT.LEFT);
+ displayLabel.setText("Type:");
+
+ mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
+ mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_*
+ mDisplayTypeCombo.add("Log All");
+ mDisplayTypeCombo.add("Filtered Log");
+ mDisplayTypeCombo.add("Graph");
+ mDisplayTypeCombo.add("Sync");
+ mDisplayTypeCombo.add("Sync Histogram");
+ mDisplayTypeCombo.add("Sync Performance");
+ mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) {
+ /* Replace the EventDisplay object with a different subclass */
+ setModified();
+ String name = eventDisplay.getName();
+ EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name);
+ setCurrentEventDisplay(newEventDisplay);
+ fillUiWith(newEventDisplay);
+ }
+ }
+ });
+
+ mChartOptions = new Group(mInfoGroup, SWT.NONE);
+ mChartOptions.setText("Chart Options");
+ GridData gd;
+ mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+ mChartOptions.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(mChartOptions, SWT.NONE);
+ l.setText("Time Limit (seconds):");
+
+ mTimeLimitText = new Text(mChartOptions, SWT.BORDER);
+ mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTimeLimitText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent arg0) {
+ String text = mTimeLimitText.getText().trim();
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ try {
+ if (text.length() == 0) {
+ eventDisplay.resetChartTimeLimit();
+ } else {
+ eventDisplay.setChartTimeLimit(Long.parseLong(text));
+ }
+ } catch (NumberFormatException nfe) {
+ eventDisplay.resetChartTimeLimit();
+ } finally {
+ setModified();
+ }
+ }
+ }
+ });
+
+ mHistOptions = new Group(mInfoGroup, SWT.NONE);
+ mHistOptions.setText("Histogram Options");
+ GridData gdh;
+ mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL));
+ gdh.horizontalSpan = 2;
+ mHistOptions.setLayout(new GridLayout(2, false));
+
+ Label lh = new Label(mHistOptions, SWT.NONE);
+ lh.setText("Histogram width (hours):");
+
+ mHistWidthText = new Text(mHistOptions, SWT.BORDER);
+ mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mHistWidthText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent arg0) {
+ String text = mHistWidthText.getText().trim();
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ try {
+ if (text.length() == 0) {
+ eventDisplay.resetHistWidth();
+ } else {
+ eventDisplay.setHistWidth(Long.parseLong(text));
+ }
+ } catch (NumberFormatException nfe) {
+ eventDisplay.resetHistWidth();
+ } finally {
+ setModified();
+ }
+ }
+ }
+ });
+
+ mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK);
+ mPidFilterCheckBox.setText("Enable filtering by pid");
+ mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+ mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection());
+ mPidText.setEnabled(mPidFilterCheckBox.getSelection());
+ setModified();
+ }
+ }
+ });
+
+ Label pidLabel = new Label(mInfoGroup, SWT.NONE);
+ pidLabel.setText("Pid Filter:");
+ pidLabel.setToolTipText("Enter all pids, separated by commas");
+
+ mPidText = new Text(mInfoGroup, SWT.BORDER);
+ mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mPidText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ if (mProcessTextChanges) {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null && eventDisplay.getPidFiltering()) {
+ String pidText = mPidText.getText().trim();
+ String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$
+
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ for (String pid : pids) {
+ try {
+ list.add(Integer.valueOf(pid));
+ } catch (NumberFormatException nfe) {
+ // just ignore non valid pid
+ }
+ }
+
+ eventDisplay.setPidFilterList(list);
+ setModified();
+ }
+ }
+ }
+ });
+
+ /* ------------------
+ * EVENT VALUE/OCCURRENCE SELECTION
+ * ------------------ */
+ mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class,
+ "Event Value Display");
+ mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class,
+ "Event Occurrence Display");
+ }
+
+ private SelectionWidgets createEventSelection(Composite rightPanel,
+ final Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+ String groupMessage) {
+
+ Group eventSelectionPanel = new Group(rightPanel, SWT.NONE);
+ eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl;
+ eventSelectionPanel.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ eventSelectionPanel.setText(groupMessage);
+
+ final SelectionWidgets widgets = new SelectionWidgets();
+
+ widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL);
+ widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH));
+ widgets.mList.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ widgets.mDeleteButton.setEnabled(true);
+ widgets.mEditButton.setEnabled(true);
+ } else {
+ widgets.mDeleteButton.setEnabled(false);
+ widgets.mEditButton.setEnabled(false);
+ }
+ }
+ });
+
+ Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE);
+ rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+ rightControls.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing = 0;
+ gl.horizontalSpacing = 0;
+
+ widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mNewButton.setText("New...");
+ widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mNewButton.setEnabled(false);
+ widgets.mNewButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ try {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ EventValueSelector dialog = new EventValueSelector(mShell);
+ if (dialog.open(descriptorClass, mLogParser)) {
+ eventDisplay.addDescriptor(dialog.getDescriptor());
+ fillUiWith(eventDisplay);
+ setModified();
+ }
+ }
+ } catch (Exception e1) {
+ e1.printStackTrace();
+ }
+ }
+ });
+
+ widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mEditButton.setText("Edit...");
+ widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mEditButton.setEnabled(false);
+ widgets.mEditButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // get the current descriptor index
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ // get the descriptor itself
+ OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor(
+ descriptorClass, index);
+
+ // open the edit dialog.
+ EventValueSelector dialog = new EventValueSelector(mShell);
+ if (dialog.open(descriptor, mLogParser)) {
+ descriptor.replaceWith(dialog.getDescriptor());
+ eventDisplay.updateValueDescriptorCheck();
+ fillUiWith(eventDisplay);
+
+ // reselect the item since fillUiWith remove the selection.
+ widgets.mList.select(index);
+ widgets.mList.notifyListeners(SWT.Selection, null);
+
+ setModified();
+ }
+ }
+ }
+ }
+ });
+
+ widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+ widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ widgets.mDeleteButton.setText("Delete");
+ widgets.mDeleteButton.setEnabled(false);
+ widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // current event
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // get the current descriptor index
+ int index = widgets.mList.getSelectionIndex();
+ if (index != -1) {
+ eventDisplay.removeDescriptor(descriptorClass, index);
+ fillUiWith(eventDisplay);
+ setModified();
+ }
+ }
+ }
+ });
+
+ return widgets;
+ }
+
+
+ private void duplicateEventDisplay(ArrayList<EventDisplay> displayList) {
+ for (EventDisplay eventDisplay : displayList) {
+ mDisplayList.add(EventDisplay.clone(eventDisplay));
+ }
+ }
+
+ private void buildPidList(ArrayList<EventContainer> eventList) {
+ mPidList = new ArrayList<Integer>();
+ for (EventContainer event : eventList) {
+ if (mPidList.indexOf(event.pid) == -1) {
+ mPidList.add(event.pid);
+ }
+ }
+ }
+
+ private void setModified() {
+ mEditStatus = true;
+ }
+
+
+ private void enable(boolean status) {
+ mEventDisplayDeleteButton.setEnabled(status);
+
+ // enable up/down
+ int selection = mEventDisplayList.getSelectionIndex();
+ int count = mEventDisplayList.getItemCount();
+ mEventDisplayUpButton.setEnabled(status && selection > 0);
+ mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1);
+
+ mDisplayNameText.setEnabled(status);
+ mDisplayTypeCombo.setEnabled(status);
+ mPidFilterCheckBox.setEnabled(status);
+
+ mValueSelection.setEnabled(status);
+ mOccurrenceSelection.setEnabled(status);
+ mValueSelection.mNewButton.setEnabled(status);
+ mOccurrenceSelection.mNewButton.setEnabled(status);
+ if (status == false) {
+ mPidText.setEnabled(false);
+ }
+ }
+
+ private void fillEventDisplayList() {
+ for (EventDisplay eventDisplay : mDisplayList) {
+ mEventDisplayList.add(eventDisplay.getName());
+ }
+ }
+
+ private void createNewEventDisplay() {
+ int count = mDisplayList.size();
+
+ String name = String.format("display %1$d", count + 1);
+
+ EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name);
+
+ mDisplayList.add(eventDisplay);
+ mEventDisplayList.add(name);
+
+ mEventDisplayList.select(count);
+ handleEventDisplaySelection();
+ mEventDisplayList.showSelection();
+
+ setModified();
+ }
+
+ private void deleteEventDisplay() {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ mDisplayList.remove(selection);
+ mEventDisplayList.remove(selection);
+ if (mDisplayList.size() < selection) {
+ selection--;
+ }
+ mEventDisplayList.select(selection);
+ handleEventDisplaySelection();
+
+ setModified();
+ }
+ }
+
+ private EventDisplay getCurrentEventDisplay() {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ return mDisplayList.get(selection);
+ }
+
+ return null;
+ }
+
+ private void setCurrentEventDisplay(EventDisplay eventDisplay) {
+ int selection = mEventDisplayList.getSelectionIndex();
+ if (selection != -1) {
+ mDisplayList.set(selection, eventDisplay);
+ }
+ }
+
+ private void handleEventDisplaySelection() {
+ EventDisplay eventDisplay = getCurrentEventDisplay();
+ if (eventDisplay != null) {
+ // enable the UI
+ enable(true);
+
+ // and fill it
+ fillUiWith(eventDisplay);
+ } else {
+ // disable the UI
+ enable(false);
+
+ // and empty it.
+ emptyUi();
+ }
+ }
+
+ private void emptyUi() {
+ mDisplayNameText.setText("");
+ mDisplayTypeCombo.clearSelection();
+ mValueSelection.mList.removeAll();
+ mOccurrenceSelection.mList.removeAll();
+ }
+
+ private void fillUiWith(EventDisplay eventDisplay) {
+ mProcessTextChanges = false;
+
+ mDisplayNameText.setText(eventDisplay.getName());
+ int displayMode = eventDisplay.getDisplayType();
+ mDisplayTypeCombo.select(displayMode);
+ if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) {
+ GridData gd = (GridData) mChartOptions.getLayoutData();
+ gd.exclude = false;
+ mChartOptions.setVisible(!gd.exclude);
+ long limit = eventDisplay.getChartTimeLimit();
+ if (limit != -1) {
+ mTimeLimitText.setText(Long.toString(limit));
+ } else {
+ mTimeLimitText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ GridData gd = (GridData) mChartOptions.getLayoutData();
+ gd.exclude = true;
+ mChartOptions.setVisible(!gd.exclude);
+ mTimeLimitText.setText(""); //$NON-NLS-1$
+ }
+
+ if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) {
+ GridData gd = (GridData) mHistOptions.getLayoutData();
+ gd.exclude = false;
+ mHistOptions.setVisible(!gd.exclude);
+ long limit = eventDisplay.getHistWidth();
+ if (limit != -1) {
+ mHistWidthText.setText(Long.toString(limit));
+ } else {
+ mHistWidthText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ GridData gd = (GridData) mHistOptions.getLayoutData();
+ gd.exclude = true;
+ mHistOptions.setVisible(!gd.exclude);
+ mHistWidthText.setText(""); //$NON-NLS-1$
+ }
+ mInfoGroup.layout(true);
+ mShell.layout(true);
+ mShell.pack();
+
+ if (eventDisplay.getPidFiltering()) {
+ mPidFilterCheckBox.setSelection(true);
+ mPidText.setEnabled(true);
+
+ // build the pid list.
+ ArrayList<Integer> list = eventDisplay.getPidFilterList();
+ if (list != null) {
+ StringBuilder sb = new StringBuilder();
+ int count = list.size();
+ for (int i = 0 ; i < count ; i++) {
+ sb.append(list.get(i));
+ if (i < count - 1) {
+ sb.append(", ");//$NON-NLS-1$
+ }
+ }
+ mPidText.setText(sb.toString());
+ } else {
+ mPidText.setText(""); //$NON-NLS-1$
+ }
+ } else {
+ mPidFilterCheckBox.setSelection(false);
+ mPidText.setEnabled(false);
+ mPidText.setText(""); //$NON-NLS-1$
+ }
+
+ mProcessTextChanges = true;
+
+ mValueSelection.mList.removeAll();
+ mOccurrenceSelection.mList.removeAll();
+
+ if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG ||
+ eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) {
+ mOccurrenceSelection.setEnabled(true);
+ mValueSelection.setEnabled(true);
+
+ Iterator<ValueDisplayDescriptor> valueIterator = eventDisplay.getValueDescriptors();
+
+ while (valueIterator.hasNext()) {
+ ValueDisplayDescriptor descriptor = valueIterator.next();
+ mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s",
+ mEventTagMap.get(descriptor.eventTag), descriptor.valueName,
+ getSeriesLabelDescription(descriptor), getFilterDescription(descriptor)));
+ }
+
+ Iterator<OccurrenceDisplayDescriptor> occurrenceIterator =
+ eventDisplay.getOccurrenceDescriptors();
+
+ while (occurrenceIterator.hasNext()) {
+ OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next();
+
+ mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s",
+ mEventTagMap.get(descriptor.eventTag),
+ getSeriesLabelDescription(descriptor),
+ getFilterDescription(descriptor)));
+ }
+
+ mValueSelection.mList.notifyListeners(SWT.Selection, null);
+ mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null);
+ } else {
+ mOccurrenceSelection.setEnabled(false);
+ mValueSelection.setEnabled(false);
+ }
+
+ }
+
+ /**
+ * Returns a String describing what is used as the series label
+ * @param descriptor the descriptor of the display.
+ */
+ private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor.seriesValueIndex != -1) {
+ if (descriptor.includePid) {
+ return String.format("%1$s + pid",
+ mEventDescriptionMap.get(
+ descriptor.eventTag)[descriptor.seriesValueIndex].getName());
+ } else {
+ return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex]
+ .getName();
+ }
+ }
+ return "pid";
+ }
+
+ private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) {
+ if (descriptor.filterValueIndex != -1) {
+ return String.format(" [%1$s %2$s %3$s]",
+ mEventDescriptionMap.get(
+ descriptor.eventTag)[descriptor.filterValueIndex].getName(),
+ descriptor.filterCompareMethod.testString(),
+ descriptor.filterValue != null ?
+ descriptor.filterValue.toString() : "?"); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java
new file mode 100644
index 0000000..a1303f6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+/**
+ * Imports a textual event log. Gets tags from build path.
+ */
+public class EventLogImporter {
+
+ private String[] mTags;
+ private String[] mLog;
+
+ public EventLogImporter(String filePath) throws FileNotFoundException {
+ String top = System.getenv("ANDROID_BUILD_TOP");
+ if (top == null) {
+ throw new FileNotFoundException();
+ }
+ final String tagFile = top + "/system/core/logcat/event-log-tags";
+ BufferedReader tagReader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(tagFile)));
+ BufferedReader eventReader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(filePath)));
+ try {
+ readTags(tagReader);
+ readLog(eventReader);
+ } catch (IOException e) {
+ }
+ }
+
+ public String[] getTags() {
+ return mTags;
+ }
+
+ public String[] getLog() {
+ return mLog;
+ }
+
+ private void readTags(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ content.add(line);
+ }
+ mTags = content.toArray(new String[content.size()]);
+ }
+
+ private void readLog(BufferedReader reader) throws IOException {
+ String line;
+
+ ArrayList<String> content = new ArrayList<String>();
+ while ((line = reader.readLine()) != null) {
+ content.add(line);
+ }
+
+ mLog = content.toArray(new String[content.size()]);
+ }
+
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java
new file mode 100644
index 0000000..2621c6a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java
@@ -0,0 +1,926 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.LogReceiver;
+import com.android.ddmlib.log.LogReceiver.ILogListener;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.IImageLoader;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.annotation.WorkerThread;
+import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Event log viewer
+ */
+public class EventLogPanel extends TablePanel implements ILogListener,
+ ILogColumnListener {
+
+ private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$
+
+ private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$
+ private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$
+
+ static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$
+ static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$
+
+ private final static int DEFAULT_DISPLAY_WIDTH = 500;
+ private final static int DEFAULT_DISPLAY_HEIGHT = 400;
+
+ private IImageLoader mImageLoader;
+
+ private Device mCurrentLoggedDevice;
+ private String mCurrentLogFile;
+ private LogReceiver mCurrentLogReceiver;
+ private EventLogParser mCurrentEventLogParser;
+
+ private Object mLock = new Object();
+
+ /** list of all the events. */
+ private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>();
+
+ /** list of all the new events, that have yet to be displayed by the ui */
+ private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>();
+ /** indicates a pending ui thread display */
+ private boolean mPendingDisplay = false;
+
+ /** list of all the custom event displays */
+ private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>();
+
+ private final NumberFormat mFormatter = NumberFormat.getInstance();
+ private Composite mParent;
+ private ScrolledComposite mBottomParentPanel;
+ private Composite mBottomPanel;
+ private ICommonAction mOptionsAction;
+ private ICommonAction mClearAction;
+ private ICommonAction mSaveAction;
+ private ICommonAction mLoadAction;
+ private ICommonAction mImportAction;
+
+ /** file containing the current log raw data. */
+ private File mTempFile = null;
+
+ public EventLogPanel(IImageLoader imageLoader) {
+ super();
+ mImageLoader = imageLoader;
+ mFormatter.setGroupingUsed(true);
+ }
+
+ /**
+ * Sets the external actions.
+ * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code
+ * when triggered by using {@link ICommonAction#setRunnable(Runnable)}.
+ * <p/>It will also make sure they are enabled only when possible.
+ * @param optionsAction
+ * @param clearAction
+ * @param saveAction
+ * @param loadAction
+ * @param importAction
+ */
+ public void setActions(ICommonAction optionsAction, ICommonAction clearAction,
+ ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) {
+ mOptionsAction = optionsAction;
+ mOptionsAction.setRunnable(new Runnable() {
+ public void run() {
+ openOptionPanel();
+ }
+ });
+
+ mClearAction = clearAction;
+ mClearAction.setRunnable(new Runnable() {
+ public void run() {
+ clearLog();
+ }
+ });
+
+ mSaveAction = saveAction;
+ mSaveAction.setRunnable(new Runnable() {
+ public void run() {
+ try {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Save Event Log");
+ fileDialog.setFileName("event.log");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ saveLog(fileName);
+ }
+ } catch (IOException e1) {
+ }
+ }
+ });
+
+ mLoadAction = loadAction;
+ mLoadAction.setRunnable(new Runnable() {
+ public void run() {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Load Event Log");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ loadLog(fileName);
+ }
+ }
+ });
+
+ mImportAction = importAction;
+ mImportAction.setRunnable(new Runnable() {
+ public void run() {
+ FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+ fileDialog.setText("Import Bug Report");
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ importBugReport(fileName);
+ }
+ }
+ });
+
+ mOptionsAction.setEnabled(false);
+ mClearAction.setEnabled(false);
+ mSaveAction.setEnabled(false);
+ }
+
+ /**
+ * Opens the option panel.
+ * </p>
+ * <b>This must be called from the UI thread</b>
+ */
+ @UiThread
+ public void openOptionPanel() {
+ try {
+ EventDisplayOptions dialog = new EventDisplayOptions(mImageLoader, mParent.getShell());
+ if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) {
+ synchronized (mLock) {
+ // get the new EventDisplay list
+ mEventDisplays.clear();
+ mEventDisplays.addAll(dialog.getEventDisplays());
+
+ // since the list of EventDisplay changed, we store it.
+ saveEventDisplays();
+
+ rebuildUi();
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("EventLog", e); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Clears the log.
+ * <p/>
+ * <b>This must be called from the UI thread</b>
+ */
+ public void clearLog() {
+ try {
+ synchronized (mLock) {
+ mEvents.clear();
+ mNewEvents.clear();
+ mPendingDisplay = false;
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resetUI();
+ }
+ }
+ } catch (SWTException e) {
+ Log.e("EventLog", e); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Saves the content of the event log into a file. The log is saved in the same
+ * binary format than on the device.
+ * @param filePath
+ * @throws IOException
+ */
+ public void saveLog(String filePath) throws IOException {
+ if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) {
+ File destFile = new File(filePath);
+ destFile.createNewFile();
+ FileInputStream fis = new FileInputStream(mTempFile);
+ FileOutputStream fos = new FileOutputStream(destFile);
+ byte[] buffer = new byte[1024];
+
+ int count;
+
+ while ((count = fis.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+
+ fos.close();
+ fis.close();
+
+ // now we save the tag file
+ filePath = filePath + TAG_FILE_EXT;
+ mCurrentEventLogParser.saveTags(filePath);
+ }
+ }
+
+ /**
+ * Loads a binary event log (if has associated .tag file) or
+ * otherwise loads a textual event log.
+ * @param filePath Event log path (and base of potential tag file)
+ */
+ public void loadLog(String filePath) {
+ if ((new File(filePath + TAG_FILE_EXT)).exists()) {
+ startEventLogFromFiles(filePath);
+ } else {
+ try {
+ EventLogImporter importer = new EventLogImporter(filePath);
+ String[] tags = importer.getTags();
+ String[] log = importer.getLog();
+ startEventLogFromContent(tags, log);
+ } catch (FileNotFoundException e) {
+ // If this fails, display the error message from startEventLogFromFiles,
+ // and pretend we never tried EventLogImporter
+ Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog",
+ String.format("Failure to read %1$s", filePath + TAG_FILE_EXT));
+ }
+
+ }
+ }
+
+ public void importBugReport(String filePath) {
+ try {
+ BugReportImporter importer = new BugReportImporter(filePath);
+
+ String[] tags = importer.getTags();
+ String[] log = importer.getLog();
+
+ startEventLogFromContent(tags, log);
+
+ } catch (FileNotFoundException e) {
+ Log.logAndDisplay(LogLevel.ERROR, "Import",
+ "Unable to import bug report: " + e.getMessage());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected()
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected()
+ */
+ @Override
+ public void deviceSelected() {
+ startEventLog(getCurrentDevice());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int)
+ */
+ public void clientChanged(Client client, int changeMask) {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+ mParent.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ synchronized (mLock) {
+ if (mCurrentLogReceiver != null) {
+ mCurrentLogReceiver.cancel();
+ mCurrentLogReceiver = null;
+ mCurrentEventLogParser = null;
+ mCurrentLoggedDevice = null;
+ mEventDisplays.clear();
+ mEvents.clear();
+ }
+ }
+ }
+ });
+
+ final IPreferenceStore store = DdmUiPreferences.getStore();
+
+ // init some store stuff
+ store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH);
+ store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT);
+
+ mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL);
+ mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mBottomParentPanel.setExpandHorizontal(true);
+ mBottomParentPanel.setExpandVertical(true);
+
+ mBottomParentPanel.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ if (mBottomPanel != null) {
+ Rectangle r = mBottomParentPanel.getClientArea();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+ SWT.DEFAULT));
+ }
+ }
+ });
+
+ prepareDisplayUi();
+
+ // load the EventDisplay from storage.
+ loadEventDisplays();
+
+ // create the ui
+ createDisplayUi();
+
+ return mBottomParentPanel;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#postCreation()
+ */
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmuilib.Panel#setFocus()
+ */
+ @Override
+ public void setFocus() {
+ mBottomParentPanel.setFocus();
+ }
+
+ /**
+ * Starts a new logcat and set mCurrentLogCat as the current receiver.
+ * @param device the device to connect logcat to.
+ */
+ private void startEventLog(final Device device) {
+ if (device == mCurrentLoggedDevice) {
+ return;
+ }
+
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ if (device != null) {
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ while (device.isOnline() == false &&
+ mCurrentLogReceiver != null &&
+ mCurrentLogReceiver.isCancelled() == false) {
+ try {
+ sleep(2000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) {
+ // logcat was stopped/cancelled before the device became ready.
+ return;
+ }
+
+ try {
+ mCurrentLoggedDevice = device;
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ mCurrentEventLogParser.init(device);
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ // prepare the temp file that will contain the raw data
+ mTempFile = File.createTempFile("android-event-", ".log");
+
+ device.runEventLogService(mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+ }
+
+ private void startEventLogFromFiles(final String fileName) {
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ mSaveAction.setEnabled(false);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ mCurrentLogFile = fileName;
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) {
+ mCurrentEventLogParser = null;
+ Log.logAndDisplay(LogLevel.ERROR, "EventLog",
+ String.format("Failure to read %1$s", fileName + TAG_FILE_EXT));
+ return;
+ }
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ runLocalEventLogService(fileName, mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+
+ private void startEventLogFromContent(final String[] tags, final String[] log) {
+ // if we have a logcat already running
+ if (mCurrentLogReceiver != null) {
+ stopEventLog(false);
+ }
+ mCurrentLoggedDevice = null;
+ mCurrentLogFile = null;
+
+ // create a new output receiver
+ mCurrentLogReceiver = new LogReceiver(this);
+
+ mSaveAction.setEnabled(false);
+
+ // start the logcat in a different thread
+ new Thread("EventLog") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ synchronized (mLock) {
+ mCurrentEventLogParser = new EventLogParser();
+ if (mCurrentEventLogParser.init(tags) == false) {
+ mCurrentEventLogParser = null;
+ return;
+ }
+ }
+
+ // update the event display with the new parser.
+ updateEventDisplays();
+
+ runLocalEventLogService(log, mCurrentLogReceiver);
+ } catch (Exception e) {
+ Log.e("EventLog", e);
+ } finally {
+ }
+ }
+ }.start();
+ }
+
+
+ public void stopEventLog(boolean inUiThread) {
+ if (mCurrentLogReceiver != null) {
+ mCurrentLogReceiver.cancel();
+
+ // when the thread finishes, no one will reference that object
+ // and it'll be destroyed
+ synchronized (mLock) {
+ mCurrentLogReceiver = null;
+ mCurrentEventLogParser = null;
+
+ mCurrentLoggedDevice = null;
+ mEvents.clear();
+ mNewEvents.clear();
+ mPendingDisplay = false;
+ }
+
+ resetUI(inUiThread);
+ }
+
+ if (mTempFile != null) {
+ mTempFile.delete();
+ mTempFile = null;
+ }
+ }
+
+ private void resetUI(boolean inUiThread) {
+ mEvents.clear();
+
+ // the ui is static we just empty it.
+ if (inUiThread) {
+ resetUiFromUiThread();
+ } else {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ resetUiFromUiThread();
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed, we're quitting. Do nothing.
+ }
+ }
+ }
+
+ private void resetUiFromUiThread() {
+ synchronized(mLock) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resetUI();
+ }
+ }
+ mOptionsAction.setEnabled(false);
+ mClearAction.setEnabled(false);
+ mSaveAction.setEnabled(false);
+ }
+
+ private void prepareDisplayUi() {
+ mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE);
+ mBottomParentPanel.setContent(mBottomPanel);
+ }
+
+ private void createDisplayUi() {
+ RowLayout rowLayout = new RowLayout();
+ rowLayout.wrap = true;
+ rowLayout.pack = false;
+ rowLayout.justify = true;
+ rowLayout.fill = true;
+ rowLayout.type = SWT.HORIZONTAL;
+ mBottomPanel.setLayout(rowLayout);
+
+ IPreferenceStore store = DdmUiPreferences.getStore();
+ int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH);
+ int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT);
+
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this);
+ if (c != null) {
+ RowData rd = new RowData();
+ rd.height = displayHeight;
+ rd.width = displayWidth;
+ c.setLayoutData(rd);
+ }
+
+ Table table = eventDisplay.getTable();
+ if (table != null) {
+ addTableToFocusListener(table);
+ }
+ }
+
+ mBottomPanel.layout();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+ mBottomParentPanel.layout();
+ }
+
+ /**
+ * Rebuild the display ui.
+ */
+ @UiThread
+ private void rebuildUi() {
+ synchronized (mLock) {
+ // we need to rebuild the ui. First we get rid of it.
+ mBottomPanel.dispose();
+ mBottomPanel = null;
+
+ prepareDisplayUi();
+ createDisplayUi();
+
+ // and fill it
+
+ boolean start_event = false;
+ synchronized (mNewEvents) {
+ mNewEvents.addAll(0, mEvents);
+
+ if (mPendingDisplay == false) {
+ mPendingDisplay = true;
+ start_event = true;
+ }
+ }
+
+ if (start_event) {
+ scheduleUIEventHandler();
+ }
+
+ Rectangle r = mBottomParentPanel.getClientArea();
+ mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+ SWT.DEFAULT));
+ }
+ }
+
+
+ /**
+ * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it.
+ * @param entry The new log entry
+ * @see LogReceiver.ILogListener#newEntry(LogEntry)
+ */
+ @WorkerThread
+ public void newEntry(LogEntry entry) {
+ synchronized (mLock) {
+ if (mCurrentEventLogParser != null) {
+ EventContainer event = mCurrentEventLogParser.parse(entry);
+ if (event != null) {
+ handleNewEvent(event);
+ }
+ }
+ }
+ }
+
+ @WorkerThread
+ private void handleNewEvent(EventContainer event) {
+ // add the event to the generic list
+ mEvents.add(event);
+
+ // add to the list of events that needs to be displayed, and trigger a
+ // new display if needed.
+ boolean start_event = false;
+ synchronized (mNewEvents) {
+ mNewEvents.add(event);
+
+ if (mPendingDisplay == false) {
+ mPendingDisplay = true;
+ start_event = true;
+ }
+ }
+
+ if (start_event == false) {
+ // we're done
+ return;
+ }
+
+ scheduleUIEventHandler();
+ }
+
+ /**
+ * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}.
+ */
+ private void scheduleUIEventHandler() {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+ d.asyncExec(new Runnable() {
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ if (mCurrentEventLogParser != null) {
+ displayNewEvents();
+ }
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // if the ui is disposed, do nothing
+ }
+ }
+
+ /**
+ * Processes raw data coming from the log service.
+ * @see LogReceiver.ILogListener#newData(byte[], int, int)
+ */
+ public void newData(byte[] data, int offset, int length) {
+ if (mTempFile != null) {
+ try {
+ FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */);
+ fos.write(data, offset, length);
+ fos.close();
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ @UiThread
+ private void displayNewEvents() {
+ // never display more than 1,000 events in this loop. We can't do too much in the UI thread.
+ int count = 0;
+
+ // prepare the displays
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.startMultiEventDisplay();
+ }
+
+ // display the new events
+ EventContainer event = null;
+ boolean need_to_reloop = false;
+ do {
+ // get the next event to display.
+ synchronized (mNewEvents) {
+ if (mNewEvents.size() > 0) {
+ if (count > 200) {
+ // there are still events to be displayed, but we don't want to hog the
+ // UI thread for too long, so we stop this runnable, but launch a new
+ // one to keep going.
+ need_to_reloop = true;
+ event = null;
+ } else {
+ event = mNewEvents.remove(0);
+ count++;
+ }
+ } else {
+ // we're done.
+ event = null;
+ mPendingDisplay = false;
+ }
+ }
+
+ if (event != null) {
+ // notify the event display
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.newEvent(event, mCurrentEventLogParser);
+ }
+ }
+ } while (event != null);
+
+ // we're done displaying events.
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.endMultiEventDisplay();
+ }
+
+ // if needed, ask the UI thread to re-run this method.
+ if (need_to_reloop) {
+ scheduleUIEventHandler();
+ }
+ }
+
+ /**
+ * Loads the {@link EventDisplay}s from the preference store.
+ */
+ private void loadEventDisplays() {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+ String storage = store.getString(PREFS_EVENT_DISPLAY);
+
+ if (storage.length() > 0) {
+ String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR));
+
+ for (String value : values) {
+ EventDisplay eventDisplay = EventDisplay.load(value);
+ if (eventDisplay != null) {
+ mEventDisplays.add(eventDisplay);
+ }
+ }
+ }
+ }
+
+ /**
+ * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store.
+ */
+ private void saveEventDisplays() {
+ IPreferenceStore store = DdmUiPreferences.getStore();
+
+ boolean first = true;
+ StringBuilder sb = new StringBuilder();
+
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ String storage = eventDisplay.getStorageString();
+ if (storage != null) {
+ if (first == false) {
+ sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR);
+ } else {
+ first = false;
+ }
+
+ sb.append(storage);
+ }
+ }
+
+ store.setValue(PREFS_EVENT_DISPLAY, sb.toString());
+ }
+
+ /**
+ * Updates the {@link EventDisplay} with the new {@link EventLogParser}.
+ * <p/>
+ * This will run asynchronously in the UI thread.
+ */
+ @WorkerThread
+ private void updateEventDisplays() {
+ try {
+ Display d = mBottomParentPanel.getDisplay();
+
+ d.asyncExec(new Runnable() {
+ public void run() {
+ if (mBottomParentPanel.isDisposed() == false) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.setNewLogParser(mCurrentEventLogParser);
+ }
+
+ mOptionsAction.setEnabled(true);
+ mClearAction.setEnabled(true);
+ if (mCurrentLogFile == null) {
+ mSaveAction.setEnabled(true);
+ } else {
+ mSaveAction.setEnabled(false);
+ }
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed: do nothing.
+ }
+ }
+
+ @UiThread
+ public void columnResized(int index, TableColumn sourceColumn) {
+ for (EventDisplay eventDisplay : mEventDisplays) {
+ eventDisplay.resizeColumn(index, sourceColumn);
+ }
+ }
+
+ /**
+ * Runs an event log service out of a local file.
+ * @param fileName the full file name of the local file containing the event log.
+ * @param logReceiver the receiver that will handle the log
+ * @throws IOException
+ */
+ @WorkerThread
+ private void runLocalEventLogService(String fileName, LogReceiver logReceiver)
+ throws IOException {
+ byte[] buffer = new byte[256];
+
+ FileInputStream fis = new FileInputStream(fileName);
+
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ logReceiver.parseNewData(buffer, 0, count);
+ }
+ }
+
+ @WorkerThread
+ private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) {
+ synchronized (mLock) {
+ for (String line : log) {
+ EventContainer event = mCurrentEventLogParser.parse(line);
+ if (event != null) {
+ handleNewEvent(event);
+ }
+ }
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java
new file mode 100644
index 0000000..dd32e2c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Set;
+
+final class EventValueSelector extends Dialog {
+ private static final int DLG_WIDTH = 400;
+ private static final int DLG_HEIGHT = 300;
+
+ private Shell mParent;
+ private Shell mShell;
+ private boolean mEditStatus;
+ private Combo mEventCombo;
+ private Combo mValueCombo;
+ private Combo mSeriesCombo;
+ private Button mDisplayPidCheckBox;
+ private Combo mFilterCombo;
+ private Combo mFilterMethodCombo;
+ private Text mFilterValue;
+ private Button mOkButton;
+
+ private EventLogParser mLogParser;
+ private OccurrenceDisplayDescriptor mDescriptor;
+
+ /** list of event integer in the order of the combo. */
+ private Integer[] mEventTags;
+
+ /** list of indices in the {@link EventValueDescription} array of the current event
+ * that are of type string. This lets us get back the {@link EventValueDescription} from the
+ * index in the Series {@link Combo}.
+ */
+ private final ArrayList<Integer> mSeriesIndices = new ArrayList<Integer>();
+
+ public EventValueSelector(Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ }
+
+ /**
+ * Opens the display option dialog to edit a new descriptor.
+ * @param decriptorClass the class of the object to instantiate. Must extend
+ * {@link OccurrenceDisplayDescriptor}
+ * @param logParser
+ * @return true if the object is to be created, false if the creation was canceled.
+ */
+ boolean open(Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+ EventLogParser logParser) {
+ try {
+ OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance();
+ setModified();
+ return open(descriptor, logParser);
+ } catch (InstantiationException e) {
+ return false;
+ } catch (IllegalAccessException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or
+ * a {@link ValueDisplayDescriptor} object.
+ * @param descriptor The descriptor to edit.
+ * @return true if the object was modified.
+ */
+ boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) {
+ // make a copy of the descriptor as we'll use a working copy.
+ if (descriptor instanceof ValueDisplayDescriptor) {
+ mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor);
+ } else if (descriptor instanceof OccurrenceDisplayDescriptor) {
+ mDescriptor = new OccurrenceDisplayDescriptor(descriptor);
+ } else {
+ return false;
+ }
+
+ mLogParser = logParser;
+
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ loadValueDescriptor();
+
+ checkValidity();
+
+ // Set the dialog size.
+ try {
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ mShell.layout();
+
+ // actually open the dialog
+ mShell.open();
+
+ // event loop until the dialog is closed.
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ return mEditStatus;
+ }
+
+ OccurrenceDisplayDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ private void createUI() {
+ GridData gd;
+
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Event Display Configuration");
+
+ mShell.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(mShell, SWT.NONE);
+ l.setText("Event:");
+
+ mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // the event tag / event name map
+ Map<Integer, String> eventTagMap = mLogParser.getTagMap();
+ Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap();
+ Set<Integer> keys = eventTagMap.keySet();
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ for (Integer i : keys) {
+ if (eventInfoMap.get(i) != null) {
+ String eventName = eventTagMap.get(i);
+ mEventCombo.add(eventName);
+
+ list.add(i);
+ }
+ }
+ mEventTags = list.toArray(new Integer[list.size()]);
+
+ mEventCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleEventComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Value:");
+
+ mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mValueCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleValueComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Series Name:");
+
+ mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mSeriesCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleSeriesComboSelection();
+ setModified();
+ }
+ });
+
+ // empty comp
+ new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+
+ mDisplayPidCheckBox = new Button(mShell, SWT.CHECK);
+ mDisplayPidCheckBox.setText("Also Show pid");
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mDescriptor.includePid = mDisplayPidCheckBox.getSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter By:");
+
+ mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleFilterComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter Method:");
+
+ mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ for (CompareMethod method : CompareMethod.values()) {
+ mFilterMethodCombo.add(method.toString());
+ }
+ mFilterMethodCombo.select(0);
+ mFilterMethodCombo.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleFilterMethodComboSelection();
+ setModified();
+ }
+ });
+
+ l = new Label(mShell, SWT.NONE);
+ l.setText("Filter Value:");
+
+ mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE);
+ mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterValue.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ if (mDescriptor.filterValueIndex != -1) {
+ // get the current selection in the event combo
+ int index = mEventCombo.getSelectionIndex();
+
+ if (index != -1) {
+ // match it to an event
+ int eventTag = mEventTags[index];
+ mDescriptor.eventTag = eventTag;
+
+ // get the EventValueDescription for this tag
+ EventValueDescription valueDesc = mLogParser.getEventInfoMap()
+ .get(eventTag)[mDescriptor.filterValueIndex];
+
+ // let the EventValueDescription convert the String value into an object
+ // of the proper type.
+ mDescriptor.filterValue = valueDesc.getObjectFromString(
+ mFilterValue.getText().trim());
+ setModified();
+ }
+ }
+ }
+ });
+
+ // add a separator spanning the 2 columns
+
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+
+ // add a composite to hold the ok/cancel button, no matter what the columns size are.
+ Composite buttonComp = new Composite(mShell, SWT.NONE);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ buttonComp.setLayoutData(gd);
+ GridLayout gl;
+ buttonComp.setLayout(gl = new GridLayout(6, true));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Composite padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mOkButton = new Button(buttonComp, SWT.PUSH);
+ mOkButton.setText("OK");
+ mOkButton.setLayoutData(new GridData(GridData.CENTER));
+ mOkButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Button cancelButton = new Button(buttonComp, SWT.PUSH);
+ cancelButton.setText("Cancel");
+ cancelButton.setLayoutData(new GridData(GridData.CENTER));
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ /* (non-Javadoc)
+ * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // cancel the edit
+ mEditStatus = false;
+ mShell.close();
+ }
+ });
+
+ padding = new Composite(mShell, SWT.NONE);
+ padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ public void handleEvent(Event event) {
+ event.doit = true;
+ }
+ });
+ }
+
+ private void setModified() {
+ mEditStatus = true;
+ }
+
+ private void handleEventComboSelection() {
+ // get the current selection in the event combo
+ int index = mEventCombo.getSelectionIndex();
+
+ if (index != -1) {
+ // match it to an event
+ int eventTag = mEventTags[index];
+ mDescriptor.eventTag = eventTag;
+
+ // get the EventValueDescription for this tag
+ EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+ // fill the combo for the values
+ mValueCombo.removeAll();
+ if (values != null) {
+ if (mDescriptor instanceof ValueDisplayDescriptor) {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+ mValueCombo.setEnabled(true);
+ for (EventValueDescription value : values) {
+ mValueCombo.add(value.toString());
+ }
+
+ if (valueDescriptor.valueIndex != -1) {
+ mValueCombo.select(valueDescriptor.valueIndex);
+ } else {
+ mValueCombo.clearSelection();
+ }
+ } else {
+ mValueCombo.setEnabled(false);
+ }
+
+ // fill the axis combo
+ mSeriesCombo.removeAll();
+ mSeriesCombo.setEnabled(false);
+ mSeriesIndices.clear();
+ int axisIndex = 0;
+ int selectionIndex = -1;
+ for (EventValueDescription value : values) {
+ if (value.getEventValueType() == EventValueType.STRING) {
+ mSeriesCombo.add(value.getName());
+ mSeriesCombo.setEnabled(true);
+ mSeriesIndices.add(axisIndex);
+
+ if (mDescriptor.seriesValueIndex != -1 &&
+ mDescriptor.seriesValueIndex == axisIndex) {
+ selectionIndex = axisIndex;
+ }
+ }
+ axisIndex++;
+ }
+
+ if (mSeriesCombo.isEnabled()) {
+ mSeriesCombo.add("default (pid)", 0 /* index */);
+ mSeriesIndices.add(0 /* index */, -1 /* value */);
+
+ // +1 because we added another item at index 0
+ mSeriesCombo.select(selectionIndex + 1);
+
+ if (selectionIndex >= 0) {
+ mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+ mDisplayPidCheckBox.setEnabled(true);
+ } else {
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.setSelection(false);
+ }
+ } else {
+ mDisplayPidCheckBox.setSelection(false);
+ mDisplayPidCheckBox.setEnabled(false);
+ }
+
+ // fill the filter combo
+ mFilterCombo.setEnabled(true);
+ mFilterCombo.removeAll();
+ mFilterCombo.add("(no filter)");
+ for (EventValueDescription value : values) {
+ mFilterCombo.add(value.toString());
+ }
+
+ // select the current filter
+ mFilterCombo.select(mDescriptor.filterValueIndex + 1);
+ mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod));
+
+ // fill the current filter value
+ if (mDescriptor.filterValueIndex != -1) {
+ EventValueDescription valueInfo = values[mDescriptor.filterValueIndex];
+ if (valueInfo.checkForType(mDescriptor.filterValue)) {
+ mFilterValue.setText(mDescriptor.filterValue.toString());
+ } else {
+ mFilterValue.setText("");
+ }
+ } else {
+ mFilterValue.setText("");
+ }
+ } else {
+ disableSubCombos();
+ }
+ } else {
+ disableSubCombos();
+ }
+
+ checkValidity();
+ }
+
+ /**
+ *
+ */
+ private void disableSubCombos() {
+ mValueCombo.removeAll();
+ mValueCombo.clearSelection();
+ mValueCombo.setEnabled(false);
+
+ mSeriesCombo.removeAll();
+ mSeriesCombo.clearSelection();
+ mSeriesCombo.setEnabled(false);
+
+ mDisplayPidCheckBox.setEnabled(false);
+ mDisplayPidCheckBox.setSelection(false);
+
+ mFilterCombo.removeAll();
+ mFilterCombo.clearSelection();
+ mFilterCombo.setEnabled(false);
+
+ mFilterValue.setEnabled(false);
+ mFilterValue.setText("");
+ mFilterMethodCombo.setEnabled(false);
+ }
+
+ private void handleValueComboSelection() {
+ ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+ // get the current selection in the value combo
+ int index = mValueCombo.getSelectionIndex();
+ valueDescriptor.valueIndex = index;
+
+ // for now set the built-in name
+
+ // get the current selection in the event combo
+ int eventIndex = mEventCombo.getSelectionIndex();
+
+ // match it to an event
+ int eventTag = mEventTags[eventIndex];
+
+ // get the EventValueDescription for this tag
+ EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+ valueDescriptor.valueName = values[index].getName();
+
+ checkValidity();
+ }
+
+ private void handleSeriesComboSelection() {
+ // get the current selection in the axis combo
+ int index = mSeriesCombo.getSelectionIndex();
+
+ // get the actual value index from the list.
+ int valueIndex = mSeriesIndices.get(index);
+
+ mDescriptor.seriesValueIndex = valueIndex;
+
+ if (index > 0) {
+ mDisplayPidCheckBox.setEnabled(true);
+ mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+ } else {
+ mDisplayPidCheckBox.setSelection(false);
+ mDisplayPidCheckBox.setEnabled(false);
+ }
+ }
+
+ private void handleFilterComboSelection() {
+ // get the current selection in the axis combo
+ int index = mFilterCombo.getSelectionIndex();
+
+ // decrement index by 1 since the item 0 means
+ // no filter (index = -1), and the rest is offset by 1
+ index--;
+
+ mDescriptor.filterValueIndex = index;
+
+ if (index != -1) {
+ mFilterValue.setEnabled(true);
+ mFilterMethodCombo.setEnabled(true);
+ if (mDescriptor.filterValue instanceof String) {
+ mFilterValue.setText((String)mDescriptor.filterValue);
+ }
+ } else {
+ mFilterValue.setText("");
+ mFilterValue.setEnabled(false);
+ mFilterMethodCombo.setEnabled(false);
+ }
+ }
+
+ private void handleFilterMethodComboSelection() {
+ // get the current selection in the axis combo
+ int index = mFilterMethodCombo.getSelectionIndex();
+ CompareMethod method = CompareMethod.values()[index];
+
+ mDescriptor.filterCompareMethod = method;
+ }
+
+ /**
+ * Returns the index of the filter method
+ * @param filterCompareMethod the {@link CompareMethod} enum.
+ */
+ private int getFilterMethodIndex(CompareMethod filterCompareMethod) {
+ CompareMethod[] values = CompareMethod.values();
+ for (int i = 0 ; i < values.length ; i++) {
+ if (values[i] == filterCompareMethod) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+
+ private void loadValueDescriptor() {
+ // get the index from the eventTag.
+ int eventIndex = 0;
+ int comboIndex = -1;
+ for (int i : mEventTags) {
+ if (i == mDescriptor.eventTag) {
+ comboIndex = eventIndex;
+ break;
+ }
+ eventIndex++;
+ }
+
+ if (comboIndex == -1) {
+ mEventCombo.clearSelection();
+ } else {
+ mEventCombo.select(comboIndex);
+ }
+
+ // get the event from the descriptor
+ handleEventComboSelection();
+ }
+
+ private void checkValidity() {
+ mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 &&
+ (((mDescriptor instanceof ValueDisplayDescriptor) == false) ||
+ mValueCombo.getSelectionIndex() != -1));
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java
new file mode 100644
index 0000000..3af1447
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.CrosshairState;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.PlotRenderingInfo;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYItemRendererState;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.ui.RectangleEdge;
+
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Stroke;
+import java.awt.geom.Line2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Custom renderer to render event occurrence. This rendered ignores the y value, and simply
+ * draws a line from min to max at the time of the item.
+ */
+public class OccurrenceRenderer extends XYLineAndShapeRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void drawItem(Graphics2D g2,
+ XYItemRendererState state,
+ Rectangle2D dataArea,
+ PlotRenderingInfo info,
+ XYPlot plot,
+ ValueAxis domainAxis,
+ ValueAxis rangeAxis,
+ XYDataset dataset,
+ int series,
+ int item,
+ CrosshairState crosshairState,
+ int pass) {
+ TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset;
+
+ // get the x value for the series/item.
+ double x = timeDataSet.getX(series, item).doubleValue();
+
+ // get the min/max of the range axis
+ double yMin = rangeAxis.getLowerBound();
+ double yMax = rangeAxis.getUpperBound();
+
+ RectangleEdge domainEdge = plot.getDomainAxisEdge();
+ RectangleEdge rangeEdge = plot.getRangeAxisEdge();
+
+ // convert the coordinates to java2d.
+ double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge);
+ double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge);
+ double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge);
+
+ // get the paint information for the series/item
+ Paint p = getItemPaint(series, item);
+ Stroke s = getItemStroke(series, item);
+
+ Line2D line = null;
+ PlotOrientation orientation = plot.getOrientation();
+ if (orientation == PlotOrientation.HORIZONTAL) {
+ line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D);
+ }
+ else if (orientation == PlotOrientation.VERTICAL) {
+ line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D);
+ }
+ g2.setPaint(p);
+ g2.setStroke(s);
+ g2.draw(line);
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java
new file mode 100644
index 0000000..108c097
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import java.awt.Color;
+
+abstract public class SyncCommon extends EventDisplay {
+
+ // State information while processing the event stream
+ private int mLastState; // 0 if event started, 1 if event stopped
+ private long mLastStartTime; // ms
+ private long mLastStopTime; //ms
+ private String mLastDetails;
+ private int mLastSyncSource; // poll, server, user, etc.
+
+ // Some common variables for sync display. These define the sync backends
+ //and how they should be displayed.
+ protected static final int CALENDAR = 0;
+ protected static final int GMAIL = 1;
+ protected static final int FEEDS = 2;
+ protected static final int CONTACTS = 3;
+ protected static final int ERRORS = 4;
+ protected static final int NUM_AUTHS = (CONTACTS + 1);
+ protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+ "Errors"};
+ protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+ Color.ORANGE, Color.RED};
+
+ // Values from data/etc/event-log-tags
+ final int EVENT_SYNC = 2720;
+ final int EVENT_TICKLE = 2742;
+ final int EVENT_SYNC_DETAILS = 2743;
+
+ protected SyncCommon(String name) {
+ super(name);
+ }
+
+ /**
+ * Resets the display.
+ */
+ @Override
+ void resetUI() {
+ mLastStartTime = 0;
+ mLastStopTime = 0;
+ mLastState = -1;
+ mLastSyncSource = -1;
+ mLastDetails = "";
+ }
+
+ /**
+ * Updates the display with a new event. This is the main entry point for
+ * each event. This method has the logic to tie together the start event,
+ * stop event, and details event into one graph item. The combined sync event
+ * is handed to the subclass via processSycnEvent. Note that the details
+ * can happen before or after the stop event.
+ *
+ * @param event The event
+ * @param logParser The parser providing the event.
+ */
+ @Override
+ void newEvent(EventContainer event, EventLogParser logParser) {
+ try {
+ if (event.mTag == EVENT_SYNC) {
+ int state = Integer.parseInt(event.getValueAsString(1));
+ if (state == 0) { // start
+ mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ mLastState = 0;
+ mLastSyncSource = Integer.parseInt(event.getValueAsString(2));
+ mLastDetails = "";
+ } else if (state == 1) { // stop
+ if (mLastState == 0) {
+ mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ if (mLastStartTime == 0) {
+ // Log starts with a stop event
+ mLastStartTime = mLastStopTime;
+ }
+ int auth = getAuth(event.getValueAsString(0));
+ processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+ true, mLastSyncSource);
+ mLastState = 1;
+ }
+ }
+ } else if (event.mTag == EVENT_SYNC_DETAILS) {
+ mLastDetails = event.getValueAsString(3);
+ if (mLastState != 0) { // Not inside event
+ long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+ if (updateTime - mLastStopTime <= 250) {
+ // Got details within 250ms after event, so delete and re-insert
+ // Details later than 250ms (arbitrary) are discarded as probably
+ // unrelated.
+ int auth = getAuth(event.getValueAsString(0));
+ processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+ false, mLastSyncSource);
+ }
+ }
+ }
+ } catch (InvalidTypeException e) {
+ }
+ }
+
+ /**
+ * Callback hook for subclass to process a sync event. newEvent has the logic
+ * to combine start and stop events and passes a processed event to the
+ * subclass.
+ *
+ * @param event The sync event
+ * @param auth The sync authority
+ * @param startTime Start time (ms) of events
+ * @param stopTime Stop time (ms) of events
+ * @param details Details associated with the event.
+ * @param newEvent True if this event is a new sync event. False if this event
+ * @param syncSource Poll, user, server, etc.
+ */
+ abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+ String details, boolean newEvent, int syncSource);
+
+ /**
+ * Converts authority name to auth number.
+ *
+ * @param authname "calendar", etc.
+ * @return number series number associated with the authority
+ */
+ protected int getAuth(String authname) throws InvalidTypeException {
+ if ("calendar".equals(authname) || "cl".equals(authname)) {
+ return CALENDAR;
+ } else if ("contacts".equals(authname) || "cp".equals(authname)) {
+ return CONTACTS;
+ } else if ("subscribedfeeds".equals(authname)) {
+ return FEEDS;
+ } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) {
+ return GMAIL;
+ } else if ("gmail-live".equals(authname)) {
+ return GMAIL;
+ } else if ("unknown".equals(authname)) {
+ return -1; // Unknown tickles; discard
+ } else {
+ throw new InvalidTypeException("Unknown authname " + authname);
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java
new file mode 100644
index 0000000..c66fe48
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmuilib.IImageLoader;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Small dialog box to edit a static port number.
+ */
+public class EditFilterDialog extends Dialog {
+
+ private static final int DLG_WIDTH = 400;
+ private static final int DLG_HEIGHT = 250;
+
+ private Shell mParent;
+
+ private Shell mShell;
+
+ private boolean mOk = false;
+
+ private IImageLoader mImageLoader;
+
+ /**
+ * Filter being edited or created
+ */
+ private LogFilter mFilter;
+
+ private String mName;
+ private String mTag;
+ private String mPid;
+
+ /** Log level as an index of the drop-down combo
+ * @see getLogLevel
+ * @see getComboIndex
+ */
+ private int mLogLevel;
+
+ private Button mOkButton;
+
+ private Label mPidWarning;
+
+ public EditFilterDialog(IImageLoader imageLoader, Shell parent) {
+ super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+ mImageLoader = imageLoader;
+ }
+
+ public EditFilterDialog(IImageLoader imageLoader, Shell shell,
+ LogFilter filter) {
+ this(imageLoader, shell);
+ mFilter = filter;
+ }
+
+ /**
+ * Opens the dialog. The method will return when the user closes the dialog
+ * somehow.
+ *
+ * @return true if ok was pressed, false if cancelled.
+ */
+ public boolean open() {
+ createUI();
+
+ if (mParent == null || mShell == null) {
+ return false;
+ }
+
+ mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+ Rectangle r = mParent.getBounds();
+ // get the center new top left.
+ int cx = r.x + r.width/2;
+ int x = cx - DLG_WIDTH / 2;
+ int cy = r.y + r.height/2;
+ int y = cy - DLG_HEIGHT / 2;
+ mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+ mShell.open();
+
+ Display display = mParent.getDisplay();
+ while (!mShell.isDisposed()) {
+ if (!display.readAndDispatch())
+ display.sleep();
+ }
+
+ // we're quitting with OK.
+ // Lets update the filter if needed
+ if (mOk) {
+ // if it was a "Create filter" action we need to create it first.
+ if (mFilter == null) {
+ mFilter = new LogFilter(mName);
+ }
+
+ // setup the filter
+ mFilter.setTagMode(mTag);
+
+ if (mPid != null && mPid.length() > 0) {
+ mFilter.setPidMode(Integer.parseInt(mPid));
+ } else {
+ mFilter.setPidMode(-1);
+ }
+
+ mFilter.setLogLevel(getLogLevel(mLogLevel));
+ }
+
+ return mOk;
+ }
+
+ public LogFilter getFilter() {
+ return mFilter;
+ }
+
+ private void createUI() {
+ mParent = getParent();
+ mShell = new Shell(mParent, getStyle());
+ mShell.setText("Log Filter");
+
+ mShell.setLayout(new GridLayout(1, false));
+
+ mShell.addListener(SWT.Close, new Listener() {
+ public void handleEvent(Event event) {
+ }
+ });
+
+ // top part with the filter name
+ Composite nameComposite = new Composite(mShell, SWT.NONE);
+ nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ nameComposite.setLayout(new GridLayout(2, false));
+
+ Label l = new Label(nameComposite, SWT.NONE);
+ l.setText("Filter Name:");
+
+ final Text filterNameText = new Text(nameComposite,
+ SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ mName = mFilter.getName();
+ if (mName != null) {
+ filterNameText.setText(mName);
+ }
+ }
+ filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ filterNameText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mName = filterNameText.getText().trim();
+ validate();
+ }
+ });
+
+ // separator
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+
+ // center part with the filter parameters
+ Composite main = new Composite(mShell, SWT.NONE);
+ main.setLayoutData(new GridData(GridData.FILL_BOTH));
+ main.setLayout(new GridLayout(3, false));
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by Log Tag:");
+
+ final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ mTag = mFilter.getTagFilter();
+ if (mTag != null) {
+ tagText.setText(mTag);
+ }
+ }
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ tagText.setLayoutData(gd);
+ tagText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mTag = tagText.getText().trim();
+ validate();
+ }
+ });
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by pid:");
+
+ final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER);
+ if (mFilter != null) {
+ if (mFilter.getPidFilter() != -1) {
+ mPid = Integer.toString(mFilter.getPidFilter());
+ } else {
+ mPid = "";
+ }
+ pidText.setText(mPid);
+ }
+ pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ pidText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mPid = pidText.getText().trim();
+ validate();
+ }
+ });
+
+ mPidWarning = new Label(main, SWT.NONE);
+ mPidWarning.setImage(mImageLoader.loadImage("empty.png", // $NON-NLS-1$
+ mShell.getDisplay()));
+
+ l = new Label(main, SWT.NONE);
+ l.setText("by Log level:");
+
+ final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ logCombo.setLayoutData(gd);
+
+ // add the labels
+ logCombo.add("<none>");
+ logCombo.add("Error");
+ logCombo.add("Warning");
+ logCombo.add("Info");
+ logCombo.add("Debug");
+ logCombo.add("Verbose");
+
+ if (mFilter != null) {
+ mLogLevel = getComboIndex(mFilter.getLogLevel());
+ logCombo.select(mLogLevel);
+ } else {
+ logCombo.select(0);
+ }
+
+ logCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // get the selection
+ mLogLevel = logCombo.getSelectionIndex();
+ validate();
+ }
+ });
+
+ // separator
+ l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // bottom part with the ok/cancel
+ Composite bottomComp = new Composite(mShell, SWT.NONE);
+ bottomComp
+ .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+ bottomComp.setLayout(new GridLayout(2, true));
+
+ mOkButton = new Button(bottomComp, SWT.NONE);
+ mOkButton.setText("OK");
+ mOkButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mOk = true;
+ mShell.close();
+ }
+ });
+ mOkButton.setEnabled(false);
+ mShell.setDefaultButton(mOkButton);
+
+ Button cancelButton = new Button(bottomComp, SWT.NONE);
+ cancelButton.setText("Cancel");
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mShell.close();
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Returns the log level from a combo index.
+ * @param index the Combo index
+ * @return a log level valid for the Log class.
+ */
+ protected int getLogLevel(int index) {
+ if (index == 0) {
+ return -1;
+ }
+
+ return 7 - index;
+ }
+
+ /**
+ * Returns the index in the combo that matches the log level
+ * @param logLevel The Log level.
+ * @return the combo index
+ */
+ private int getComboIndex(int logLevel) {
+ if (logLevel == -1) {
+ return 0;
+ }
+
+ return 7 - logLevel;
+ }
+
+ /**
+ * Validates the content of the 2 text fields and enable/disable "ok", while
+ * setting up the warning/error message.
+ */
+ private void validate() {
+
+ // then we check it only contains digits.
+ if (mPid != null) {
+ if (mPid.matches("[0-9]*") == false) { // $NON-NLS-1$
+ mOkButton.setEnabled(false);
+ mPidWarning.setImage(mImageLoader.loadImage(
+ "warning.png", // $NON-NLS-1$
+ mShell.getDisplay()));
+ return;
+ } else {
+ mPidWarning.setImage(mImageLoader.loadImage(
+ "empty.png", // $NON-NLS-1$
+ mShell.getDisplay()));
+ }
+ }
+
+ if (mName == null || mName.length() == 0) {
+ mOkButton.setEnabled(false);
+ return;
+ }
+
+ mOkButton.setEnabled(true);
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java
new file mode 100644
index 0000000..9cff656
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import org.eclipse.swt.graphics.Color;
+
+public class LogColors {
+ public Color infoColor;
+ public Color debugColor;
+ public Color errorColor;
+ public Color warningColor;
+ public Color verboseColor;
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java
new file mode 100644
index 0000000..a32de2f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.logcat.LogPanel.LogMessage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.regex.PatternSyntaxException;
+
+/** logcat output filter class */
+public class LogFilter {
+
+ public final static int MODE_PID = 0x01;
+ public final static int MODE_TAG = 0x02;
+ public final static int MODE_LEVEL = 0x04;
+
+ private String mName;
+
+ /**
+ * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL
+ */
+ private int mMode = 0;
+
+ /**
+ * pid used for filtering. Only valid if mMode is MODE_PID.
+ */
+ private int mPid;
+
+ /** Single level log level as defined in Log.mLevelChar. Only valid
+ * if mMode is MODE_LEVEL */
+ private int mLogLevel;
+
+ /**
+ * log tag filtering. Only valid if mMode is MODE_TAG
+ */
+ private String mTag;
+
+ private Table mTable;
+ private TabItem mTabItem;
+ private boolean mIsCurrentTabItem = false;
+ private int mUnreadCount = 0;
+
+ /** Temp keyword filtering */
+ private String[] mTempKeywordFilters;
+
+ /** temp pid filtering */
+ private int mTempPid = -1;
+
+ /** temp tag filtering */
+ private String mTempTag;
+
+ /** temp log level filtering */
+ private int mTempLogLevel = -1;
+
+ private LogColors mColors;
+
+ private boolean mTempFilteringStatus = false;
+
+ private final ArrayList<LogMessage> mMessages = new ArrayList<LogMessage>();
+ private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>();
+
+ private boolean mSupportsDelete = true;
+ private boolean mSupportsEdit = true;
+ private int mRemovedMessageCount = 0;
+
+ /**
+ * Creates a filter with a particular mode.
+ * @param name The name to be displayed in the UI
+ */
+ public LogFilter(String name) {
+ mName = name;
+ }
+
+ public LogFilter() {
+
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(mName);
+
+ sb.append(':');
+ sb.append(mMode);
+ if ((mMode & MODE_PID) == MODE_PID) {
+ sb.append(':');
+ sb.append(mPid);
+ }
+
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ sb.append(':');
+ sb.append(mLogLevel);
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG) {
+ sb.append(':');
+ sb.append(mTag);
+ }
+
+ return sb.toString();
+ }
+
+ public boolean loadFromString(String string) {
+ String[] segments = string.split(":"); // $NON-NLS-1$
+ int index = 0;
+
+ // get the name
+ mName = segments[index++];
+
+ // get the mode
+ mMode = Integer.parseInt(segments[index++]);
+
+ if ((mMode & MODE_PID) == MODE_PID) {
+ mPid = Integer.parseInt(segments[index++]);
+ }
+
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ mLogLevel = Integer.parseInt(segments[index++]);
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG) {
+ mTag = segments[index++];
+ }
+
+ return true;
+ }
+
+
+ /** Sets the name of the filter. */
+ void setName(String name) {
+ mName = name;
+ }
+
+ /**
+ * Returns the UI display name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Set the Table ui widget associated with this filter.
+ * @param tabItem The item in the TabFolder
+ * @param table The Table object
+ */
+ public void setWidgets(TabItem tabItem, Table table) {
+ mTable = table;
+ mTabItem = tabItem;
+ }
+
+ /**
+ * Returns true if the filter is ready for ui.
+ */
+ public boolean uiReady() {
+ return (mTable != null && mTabItem != null);
+ }
+
+ /**
+ * Returns the UI table object.
+ * @return
+ */
+ public Table getTable() {
+ return mTable;
+ }
+
+ public void dispose() {
+ mTable.dispose();
+ mTabItem.dispose();
+ mTable = null;
+ mTabItem = null;
+ }
+
+ /**
+ * Resets the filtering mode to be 0 (i.e. no filter).
+ */
+ public void resetFilteringMode() {
+ mMode = 0;
+ }
+
+ /**
+ * Returns the current filtering mode.
+ * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL
+ */
+ public int getFilteringMode() {
+ return mMode;
+ }
+
+ /**
+ * Adds PID to the current filtering mode.
+ * @param pid
+ */
+ public void setPidMode(int pid) {
+ if (pid != -1) {
+ mMode |= MODE_PID;
+ } else {
+ mMode &= ~MODE_PID;
+ }
+ mPid = pid;
+ }
+
+ /** Returns the pid filter if valid, otherwise -1 */
+ public int getPidFilter() {
+ if ((mMode & MODE_PID) == MODE_PID)
+ return mPid;
+ return -1;
+ }
+
+ public void setTagMode(String tag) {
+ if (tag != null && tag.length() > 0) {
+ mMode |= MODE_TAG;
+ } else {
+ mMode &= ~MODE_TAG;
+ }
+ mTag = tag;
+ }
+
+ public String getTagFilter() {
+ if ((mMode & MODE_TAG) == MODE_TAG)
+ return mTag;
+ return null;
+ }
+
+ public void setLogLevel(int level) {
+ if (level == -1) {
+ mMode &= ~MODE_LEVEL;
+ } else {
+ mMode |= MODE_LEVEL;
+ mLogLevel = level;
+ }
+
+ }
+
+ public int getLogLevel() {
+ if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+ return mLogLevel;
+ }
+
+ return -1;
+ }
+
+
+ public boolean supportsDelete() {
+ return mSupportsDelete ;
+ }
+
+ public boolean supportsEdit() {
+ return mSupportsEdit;
+ }
+
+ /**
+ * Sets the selected state of the filter.
+ * @param selected selection state.
+ */
+ public void setSelectedState(boolean selected) {
+ if (selected) {
+ if (mTabItem != null) {
+ mTabItem.setText(mName);
+ }
+ mUnreadCount = 0;
+ }
+ mIsCurrentTabItem = selected;
+ }
+
+ /**
+ * Adds a new message and optionally removes an old message.
+ * <p/>The new message is filtered through {@link #accept(LogMessage)}.
+ * Calls to {@link #flush()} from a UI thread will display it (and other
+ * pending messages) to the associated {@link Table}.
+ * @param logMessage the MessageData object to filter
+ * @return true if the message was accepted.
+ */
+ public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) {
+ synchronized (mMessages) {
+ if (oldMessage != null) {
+ int index = mMessages.indexOf(oldMessage);
+ if (index != -1) {
+ // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+ mMessages.remove(index);
+ mRemovedMessageCount++;
+ }
+
+ // now we look for it in mNewMessages. This can happen if the new message is added
+ // and then removed because too many messages are added between calls to #flush()
+ index = mNewMessages.indexOf(oldMessage);
+ if (index != -1) {
+ // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+ mNewMessages.remove(index);
+ }
+ }
+
+ boolean filter = accept(newMessage);
+
+ if (filter) {
+ // at this point the message is accepted, we add it to the list
+ mMessages.add(newMessage);
+ mNewMessages.add(newMessage);
+ }
+
+ return filter;
+ }
+ }
+
+ /**
+ * Removes all the items in the filter and its {@link Table}.
+ */
+ public void clear() {
+ mRemovedMessageCount = 0;
+ mNewMessages.clear();
+ mMessages.clear();
+ mTable.removeAll();
+ }
+
+ /**
+ * Filters a message.
+ * @param logMessage the Message
+ * @return true if the message is accepted by the filter.
+ */
+ boolean accept(LogMessage logMessage) {
+ // do the regular filtering now
+ if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) {
+ return false;
+ }
+
+ if ((mMode & MODE_TAG) == MODE_TAG && (
+ logMessage.data.tag == null ||
+ logMessage.data.tag.equals(mTag) == false)) {
+ return false;
+ }
+
+ int msgLogLevel = logMessage.data.logLevel.getPriority();
+
+ // test the temp log filtering first, as it replaces the old one
+ if (mTempLogLevel != -1) {
+ if (mTempLogLevel > msgLogLevel) {
+ return false;
+ }
+ } else if ((mMode & MODE_LEVEL) == MODE_LEVEL &&
+ mLogLevel > msgLogLevel) {
+ return false;
+ }
+
+ // do the temp filtering now.
+ if (mTempKeywordFilters != null) {
+ String msg = logMessage.msg;
+
+ for (String kw : mTempKeywordFilters) {
+ try {
+ if (msg.contains(kw) == false && msg.matches(kw) == false) {
+ return false;
+ }
+ } catch (PatternSyntaxException e) {
+ // if the string is not a valid regular expression,
+ // this exception is thrown.
+ return false;
+ }
+ }
+ }
+
+ if (mTempPid != -1 && mTempPid != logMessage.data.pid) {
+ return false;
+ }
+
+ if (mTempTag != null && mTempTag.length() > 0) {
+ if (mTempTag.equals(logMessage.data.tag) == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Takes all the accepted messages and display them.
+ * This must be called from a UI thread.
+ */
+ @UiThread
+ public void flush() {
+ // if scroll bar is at the bottom, we will scroll
+ ScrollBar bar = mTable.getVerticalBar();
+ boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+ // if we are not going to scroll, get the current first item being shown.
+ int topIndex = mTable.getTopIndex();
+
+ // disable drawing
+ mTable.setRedraw(false);
+
+ int totalCount = mNewMessages.size();
+
+ try {
+ // remove the items of the old messages.
+ for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) {
+ mTable.remove(0);
+ }
+
+ if (mUnreadCount > mTable.getItemCount()) {
+ mUnreadCount = mTable.getItemCount();
+ }
+
+ // add the new items
+ for (int i = 0 ; i < totalCount ; i++) {
+ LogMessage msg = mNewMessages.get(i);
+ addTableItem(msg);
+ }
+ } catch (SWTException e) {
+ // log the error and keep going. Content of the logcat table maybe unexpected
+ // but at least ddms won't crash.
+ Log.e("LogFilter", e);
+ }
+
+ // redraw
+ mTable.setRedraw(true);
+
+ // scroll if needed, by showing the last item
+ if (scroll) {
+ totalCount = mTable.getItemCount();
+ if (totalCount > 0) {
+ mTable.showItem(mTable.getItem(totalCount-1));
+ }
+ } else if (mRemovedMessageCount > 0) {
+ // we need to make sure the topIndex is still visible.
+ // Because really old items are removed from the list, this could make it disappear
+ // if we don't change the scroll value at all.
+
+ topIndex -= mRemovedMessageCount;
+ if (topIndex < 0) {
+ // looks like it disappeared. Lets just show the first item
+ mTable.showItem(mTable.getItem(0));
+ } else {
+ mTable.showItem(mTable.getItem(topIndex));
+ }
+ }
+
+ // if this filter is not the current one, we update the tab text
+ // with the amount of unread message
+ if (mIsCurrentTabItem == false) {
+ mUnreadCount += mNewMessages.size();
+ totalCount = mTable.getItemCount();
+ if (mUnreadCount > 0) {
+ mTabItem.setText(mName + " (" // $NON-NLS-1$
+ + (mUnreadCount > totalCount ? totalCount : mUnreadCount)
+ + ")"); // $NON-NLS-1$
+ } else {
+ mTabItem.setText(mName); // $NON-NLS-1$
+ }
+ }
+
+ mNewMessages.clear();
+ }
+
+ void setColors(LogColors colors) {
+ mColors = colors;
+ }
+
+ int getUnreadCount() {
+ return mUnreadCount;
+ }
+
+ void setUnreadCount(int unreadCount) {
+ mUnreadCount = unreadCount;
+ }
+
+ void setSupportsDelete(boolean support) {
+ mSupportsDelete = support;
+ }
+
+ void setSupportsEdit(boolean support) {
+ mSupportsEdit = support;
+ }
+
+ void setTempKeywordFiltering(String[] segments) {
+ mTempKeywordFilters = segments;
+ mTempFilteringStatus = true;
+ }
+
+ void setTempPidFiltering(int pid) {
+ mTempPid = pid;
+ mTempFilteringStatus = true;
+ }
+
+ void setTempTagFiltering(String tag) {
+ mTempTag = tag;
+ mTempFilteringStatus = true;
+ }
+
+ void resetTempFiltering() {
+ if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) {
+ mTempFilteringStatus = true;
+ }
+
+ mTempPid = -1;
+ mTempTag = null;
+ mTempKeywordFilters = null;
+ }
+
+ void resetTempFilteringStatus() {
+ mTempFilteringStatus = false;
+ }
+
+ boolean getTempFilterStatus() {
+ return mTempFilteringStatus;
+ }
+
+
+ /**
+ * Add a TableItem for the index-th item of the buffer
+ * @param filter The index of the table in which to insert the item.
+ */
+ private void addTableItem(LogMessage msg) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(0, msg.data.time);
+ item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() }));
+ item.setText(2, msg.data.pidString);
+ item.setText(3, msg.data.tag);
+ item.setText(4, msg.msg);
+
+ // add the buffer index as data
+ item.setData(msg);
+
+ if (msg.data.logLevel == LogLevel.INFO) {
+ item.setForeground(mColors.infoColor);
+ } else if (msg.data.logLevel == LogLevel.DEBUG) {
+ item.setForeground(mColors.debugColor);
+ } else if (msg.data.logLevel == LogLevel.ERROR) {
+ item.setForeground(mColors.errorColor);
+ } else if (msg.data.logLevel == LogLevel.WARN) {
+ item.setForeground(mColors.warningColor);
+ } else {
+ item.setForeground(mColors.verboseColor);
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java
new file mode 100644
index 0000000..bd8b75c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java
@@ -0,0 +1,1571 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.IImageLoader;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.actions.ICommonAction;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LogPanel extends SelectionDependentPanel {
+
+ private static final int STRING_BUFFER_LENGTH = 10000;
+
+ /** no filtering. Only one tab with everything. */
+ public static final int FILTER_NONE = 0;
+ /** manual mode for filter. all filters are manually created. */
+ public static final int FILTER_MANUAL = 1;
+ /** automatic mode for filter (pid mode).
+ * All filters are automatically created. */
+ public static final int FILTER_AUTO_PID = 2;
+ /** automatic mode for filter (tag mode).
+ * All filters are automatically created. */
+ public static final int FILTER_AUTO_TAG = 3;
+ /** Manual filtering mode + new filter for debug app, if needed */
+ public static final int FILTER_DEBUG = 4;
+
+ public static final int COLUMN_MODE_MANUAL = 0;
+ public static final int COLUMN_MODE_AUTO = 1;
+
+ public static String PREFS_TIME;
+ public static String PREFS_LEVEL;
+ public static String PREFS_PID;
+ public static String PREFS_TAG;
+ public static String PREFS_MESSAGE;
+
+ /**
+ * This pattern is meant to parse the first line of a log message with the option
+ * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
+ * following lines are the message (can be several line).<br>
+ * This first line looks something like<br>
+ * <code>"[ 00-00 00:00:00.000 &lt;pid&gt;:0x&lt;???&gt; &lt;severity&gt;/&lt;tag&gt;]"</code>
+ * <br>
+ * Note: severity is one of V, D, I, W, or EM<br>
+ * Note: the fraction of second value can have any number of digit.
+ * Note the tag should be trim as it may have spaces at the end.
+ */
+ private static Pattern sLogPattern = Pattern.compile(
+ "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$
+ "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$
+
+ /**
+ * Interface for Storage Filter manager. Implementation of this interface
+ * provide a custom way to archive an reload filters.
+ */
+ public interface ILogFilterStorageManager {
+
+ public LogFilter[] getFilterFromStore();
+
+ public void saveFilters(LogFilter[] filters);
+
+ public boolean requiresDefaultFilter();
+ }
+
+ private Composite mParent;
+ private IPreferenceStore mStore;
+
+ /** top object in the view */
+ private TabFolder mFolders;
+
+ private LogColors mColors;
+
+ private ILogFilterStorageManager mFilterStorage;
+
+ private LogCatOuputReceiver mCurrentLogCat;
+
+ /**
+ * Circular buffer containing the logcat output. This is unfiltered.
+ * The valid content goes from <code>mBufferStart</code> to
+ * <code>mBufferEnd - 1</code>. Therefore its number of item is
+ * <code>mBufferEnd - mBufferStart</code>.
+ */
+ private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH];
+
+ /** Represents the oldest message in the buffer */
+ private int mBufferStart = -1;
+
+ /**
+ * Represents the next usable item in the buffer to receive new message.
+ * This can be equal to mBufferStart, but when used mBufferStart will be
+ * incremented as well.
+ */
+ private int mBufferEnd = -1;
+
+ /** Filter list */
+ private LogFilter[] mFilters;
+
+ /** Default filter */
+ private LogFilter mDefaultFilter;
+
+ /** Current filter being displayed */
+ private LogFilter mCurrentFilter;
+
+ /** Filtering mode */
+ private int mFilterMode = FILTER_NONE;
+
+ /** Device currently running logcat */
+ private Device mCurrentLoggedDevice = null;
+
+ private ICommonAction mDeleteFilterAction;
+ private ICommonAction mEditFilterAction;
+
+ private ICommonAction[] mLogLevelActions;
+
+ /** message data, separated from content for multi line messages */
+ protected static class LogMessageInfo {
+ public LogLevel logLevel;
+ public int pid;
+ public String pidString;
+ public String tag;
+ public String time;
+ }
+
+ /** pointer to the latest LogMessageInfo. this is used for multi line
+ * log message, to reuse the info regarding level, pid, etc...
+ */
+ private LogMessageInfo mLastMessageInfo = null;
+
+ private boolean mPendingAsyncRefresh = false;
+
+ /** loader for the images. the implementation will varie between standalone
+ * app and eclipse plugin app and eclipse plugin. */
+ private IImageLoader mImageLoader;
+
+ private String mDefaultLogSave;
+
+ private int mColumnMode = COLUMN_MODE_MANUAL;
+ private Font mDisplayFont;
+
+ private ITableFocusListener mGlobalListener;
+
+ /** message data, separated from content for multi line messages */
+ protected static class LogMessage {
+ public LogMessageInfo data;
+ public String msg;
+
+ @Override
+ public String toString() {
+ return data.time + ": " //$NON-NLS-1$
+ + data.logLevel + "/" //$NON-NLS-1$
+ + data.tag + "(" //$NON-NLS-1$
+ + data.pidString + "): " //$NON-NLS-1$
+ + msg;
+ }
+ }
+
+ /**
+ * objects able to receive the output of a remote shell command,
+ * specifically a logcat command in this case
+ */
+ private final class LogCatOuputReceiver extends MultiLineReceiver {
+
+ public boolean isCancelled = false;
+
+ public LogCatOuputReceiver() {
+ super();
+
+ setTrimLine(false);
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ if (isCancelled == false) {
+ processLogLines(lines);
+ }
+ }
+
+ public boolean isCancelled() {
+ return isCancelled;
+ }
+ }
+
+ /**
+ * Parser class for the output of a "ps" shell command executed on a device.
+ * This class looks for a specific pid to find the process name from it.
+ * Once found, the name is used to update a filter and a tab object
+ *
+ */
+ private class PsOutputReceiver extends MultiLineReceiver {
+
+ private LogFilter mFilter;
+
+ private TabItem mTabItem;
+
+ private int mPid;
+
+ /** set to true when we've found the pid we're looking for */
+ private boolean mDone = false;
+
+ PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) {
+ mPid = pid;
+ mFilter = filter;
+ mTabItem = tabItem;
+ }
+
+ public boolean isCancelled() {
+ return mDone;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.startsWith("USER")) { //$NON-NLS-1$
+ continue;
+ }
+ // get the pid.
+ int index = line.indexOf(' ');
+ if (index == -1) {
+ continue;
+ }
+ // look for the next non blank char
+ index++;
+ while (line.charAt(index) == ' ') {
+ index++;
+ }
+
+ // this is the start of the pid.
+ // look for the end.
+ int index2 = line.indexOf(' ', index);
+
+ // get the line
+ String pidStr = line.substring(index, index2);
+ int pid = Integer.parseInt(pidStr);
+ if (pid != mPid) {
+ continue;
+ } else {
+ // get the process name
+ index = line.lastIndexOf(' ');
+ final String name = line.substring(index + 1);
+
+ mFilter.setName(name);
+
+ // update the tab
+ Display d = mFolders.getDisplay();
+ d.asyncExec(new Runnable() {
+ public void run() {
+ mTabItem.setText(name);
+ }
+ });
+
+ // we're done with this ps.
+ mDone = true;
+ return;
+ }
+ }
+ }
+
+ }
+
+
+ /**
+ * Create the log view with some default parameters
+ * @param imageLoader the image loader.
+ * @param colors The display color object
+ * @param filterStorage the storage for user defined filters.
+ * @param mode The filtering mode
+ */
+ public LogPanel(IImageLoader imageLoader, LogColors colors,
+ ILogFilterStorageManager filterStorage, int mode) {
+ mImageLoader = imageLoader;
+ mColors = colors;
+ mFilterMode = mode;
+ mFilterStorage = filterStorage;
+ mStore = DdmUiPreferences.getStore();
+ }
+
+ public void setActions(ICommonAction deleteAction, ICommonAction editAction,
+ ICommonAction[] logLevelActions) {
+ mDeleteFilterAction = deleteAction;
+ mEditFilterAction = editAction;
+ mLogLevelActions = logLevelActions;
+ }
+
+ /**
+ * Sets the column mode. Must be called before creatUI
+ * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and
+ * COLUMN_MODE_AUTO
+ */
+ public void setColumnMode(int mode) {
+ mColumnMode = mode;
+ }
+
+ /**
+ * Sets the display font.
+ * @param font The display font.
+ */
+ public void setFont(Font font) {
+ mDisplayFont = font;
+
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ Table table = f.getTable();
+ if (table != null) {
+ table.setFont(font);
+ }
+ }
+ }
+
+ if (mDefaultFilter != null) {
+ Table table = mDefaultFilter.getTable();
+ if (table != null) {
+ table.setFont(font);
+ }
+ }
+ }
+
+ /**
+ * Sent when a new device is selected. The new device can be accessed
+ * with {@link #getCurrentDevice()}.
+ */
+ @Override
+ public void deviceSelected() {
+ startLogCat(getCurrentDevice());
+ }
+
+ /**
+ * Sent when a new client is selected. The new client can be accessed
+ * with {@link #getCurrentClient()}.
+ */
+ @Override
+ public void clientSelected() {
+ // pass
+ }
+
+
+ /**
+ * Creates a control capable of displaying some information. This is
+ * called once, when the application is initializing, from the UI thread.
+ */
+ @Override
+ protected Control createControl(Composite parent) {
+ mParent = parent;
+
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+ top.setLayout(new GridLayout(1, false));
+
+ // create the tab folder
+ mFolders = new TabFolder(top, SWT.NONE);
+ mFolders.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mFolders.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mCurrentFilter != null) {
+ mCurrentFilter.setSelectedState(false);
+ }
+ mCurrentFilter = getCurrentFilter();
+ mCurrentFilter.setSelectedState(true);
+ updateColumns(mCurrentFilter.getTable());
+ if (mCurrentFilter.getTempFilterStatus()) {
+ initFilter(mCurrentFilter);
+ }
+ selectionChanged(mCurrentFilter);
+ }
+ });
+
+
+ Composite bottom = new Composite(top, SWT.NONE);
+ bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ bottom.setLayout(new GridLayout(3, false));
+
+ Label label = new Label(bottom, SWT.NONE);
+ label.setText("Filter:");
+
+ final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER);
+ filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ filterText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ updateFilteringWith(filterText.getText());
+ }
+ });
+
+ /*
+ Button addFilterBtn = new Button(bottom, SWT.NONE);
+ addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$
+ addFilterBtn.getDisplay()));
+ */
+
+ // get the filters
+ createFilters();
+
+ // for each filter, create a tab.
+ int index = 0;
+
+ if (mDefaultFilter != null) {
+ createTab(mDefaultFilter, index++, false);
+ }
+
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ createTab(f, index++, false);
+ }
+ }
+
+ return top;
+ }
+
+ @Override
+ protected void postCreation() {
+ // pass
+ }
+
+ /**
+ * Sets the focus to the proper object.
+ */
+ @Override
+ public void setFocus() {
+ mFolders.setFocus();
+ }
+
+
+ /**
+ * Starts a new logcat and set mCurrentLogCat as the current receiver.
+ * @param device the device to connect logcat to.
+ */
+ public void startLogCat(final Device device) {
+ if (device == mCurrentLoggedDevice) {
+ return;
+ }
+
+ // if we have a logcat already running
+ if (mCurrentLoggedDevice != null) {
+ stopLogCat(false);
+ mCurrentLoggedDevice = null;
+ }
+
+ resetUI(false);
+
+ if (device != null) {
+ // create a new output receiver
+ mCurrentLogCat = new LogCatOuputReceiver();
+
+ // start the logcat in a different thread
+ new Thread("Logcat") { //$NON-NLS-1$
+ @Override
+ public void run() {
+
+ while (device.isOnline() == false &&
+ mCurrentLogCat != null &&
+ mCurrentLogCat.isCancelled == false) {
+ try {
+ sleep(2000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) {
+ // logcat was stopped/cancelled before the device became ready.
+ return;
+ }
+
+ try {
+ mCurrentLoggedDevice = device;
+ device.executeShellCommand("logcat -v long", mCurrentLogCat); //$NON-NLS-1$
+ } catch (Exception e) {
+ Log.e("Logcat", e);
+ } finally {
+ // at this point the command is terminated.
+ mCurrentLogCat = null;
+ mCurrentLoggedDevice = null;
+ }
+ }
+ }.start();
+ }
+ }
+
+ /** Stop the current logcat */
+ public void stopLogCat(boolean inUiThread) {
+ if (mCurrentLogCat != null) {
+ mCurrentLogCat.isCancelled = true;
+
+ // when the thread finishes, no one will reference that object
+ // and it'll be destroyed
+ mCurrentLogCat = null;
+
+ // reset the content buffer
+ for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+ mBuffer[i] = null;
+ }
+
+ // because it's a circular buffer, it's hard to know if
+ // the array is empty with both start/end at 0 or if it's full
+ // with both start/end at 0 as well. So to mean empty, we use -1
+ mBufferStart = -1;
+ mBufferEnd = -1;
+
+ resetFilters();
+ resetUI(inUiThread);
+ }
+ }
+
+ /**
+ * Adds a new Filter. This methods displays the UI to create the filter
+ * and set up its parameters.<br>
+ * <b>MUST</b> be called from the ui thread.
+ *
+ */
+ public void addFilter() {
+ EditFilterDialog dlg = new EditFilterDialog(mImageLoader,
+ mFolders.getShell());
+ if (dlg.open()) {
+ synchronized (mBuffer) {
+ // get the new filter in the array
+ LogFilter filter = dlg.getFilter();
+ addFilterToArray(filter);
+
+ int index = mFilters.length - 1;
+ if (mDefaultFilter != null) {
+ index++;
+ }
+
+ if (false) {
+
+ for (LogFilter f : mFilters) {
+ if (f.uiReady()) {
+ f.dispose();
+ }
+ }
+ if (mDefaultFilter != null && mDefaultFilter.uiReady()) {
+ mDefaultFilter.dispose();
+ }
+
+ // for each filter, create a tab.
+ int i = 0;
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ createTab(f, i++, true);
+ }
+ }
+ if (mDefaultFilter != null) {
+ createTab(mDefaultFilter, i++, true);
+ }
+ } else {
+
+ // create ui for the filter.
+ createTab(filter, index, true);
+
+ // reset the default as it shouldn't contain the content of
+ // this new filter.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+ }
+
+ // select the new filter
+ if (mCurrentFilter != null) {
+ mCurrentFilter.setSelectedState(false);
+ }
+ mFolders.setSelection(index);
+ filter.setSelectedState(true);
+ mCurrentFilter = filter;
+
+ selectionChanged(filter);
+
+ // finally we update the filtering mode if needed
+ if (mFilterMode == FILTER_NONE) {
+ mFilterMode = FILTER_MANUAL;
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+
+ }
+ }
+ }
+
+ /**
+ * Edits the current filter. The method displays the UI to edit the filter.
+ */
+ public void editFilter() {
+ if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+ EditFilterDialog dlg = new EditFilterDialog(mImageLoader,
+ mFolders.getShell(),
+ mCurrentFilter);
+ if (dlg.open()) {
+ synchronized (mBuffer) {
+ // at this point the filter has been updated.
+ // so we update its content
+ initFilter(mCurrentFilter);
+
+ // and the content of the "other" filter as well.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+ }
+ }
+ }
+ }
+
+ /**
+ * Deletes the current filter.
+ */
+ public void deleteFilter() {
+ synchronized (mBuffer) {
+ if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+ // remove the filter from the list
+ removeFilterFromArray(mCurrentFilter);
+ mCurrentFilter.dispose();
+
+ // select the new filter
+ mFolders.setSelection(0);
+ if (mFilters.length > 0) {
+ mCurrentFilter = mFilters[0];
+ } else {
+ mCurrentFilter = mDefaultFilter;
+ }
+
+ selectionChanged(mCurrentFilter);
+
+ // update the content of the "other" filter to include what was filtered out
+ // by the deleted filter.
+ if (mDefaultFilter != null) {
+ initDefaultFilter();
+ }
+
+ mFilterStorage.saveFilters(mFilters);
+ }
+ }
+ }
+
+ /**
+ * saves the current selection in a text file.
+ * @return false if the saving failed.
+ */
+ public boolean save() {
+ synchronized (mBuffer) {
+ FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE);
+ String fileName;
+
+ dlg.setText("Save log...");
+ dlg.setFileName("log.txt");
+ String defaultPath = mDefaultLogSave;
+ if (defaultPath == null) {
+ defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+ dlg.setFilterPath(defaultPath);
+ dlg.setFilterNames(new String[] {
+ "Text Files (*.txt)"
+ });
+ dlg.setFilterExtensions(new String[] {
+ "*.txt"
+ });
+
+ fileName = dlg.open();
+ if (fileName != null) {
+ mDefaultLogSave = dlg.getFilterPath();
+
+ // get the current table and its selection
+ Table currentTable = mCurrentFilter.getTable();
+
+ int[] selection = currentTable.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // loop on the selection and output the file.
+ try {
+ FileWriter writer = new FileWriter(fileName);
+
+ for (int i : selection) {
+ TableItem item = currentTable.getItem(i);
+ LogMessage msg = (LogMessage)item.getData();
+ String line = msg.toString();
+ writer.write(line);
+ writer.write('\n');
+ }
+ writer.flush();
+
+ } catch (IOException e) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Empty the current circular buffer.
+ */
+ public void clear() {
+ synchronized (mBuffer) {
+ for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+ mBuffer[i] = null;
+ }
+
+ mBufferStart = -1;
+ mBufferEnd = -1;
+
+ // now we clear the existing filters
+ for (LogFilter filter : mFilters) {
+ filter.clear();
+ }
+
+ // and the default one
+ if (mDefaultFilter != null) {
+ mDefaultFilter.clear();
+ }
+ }
+ }
+
+ /**
+ * Copies the current selection of the current filter as multiline text.
+ *
+ * @param clipboard The clipboard to place the copied content.
+ */
+ public void copy(Clipboard clipboard) {
+ // get the current table and its selection
+ Table currentTable = mCurrentFilter.getTable();
+
+ copyTable(clipboard, currentTable);
+ }
+
+ /**
+ * Selects all lines.
+ */
+ public void selectAll() {
+ Table currentTable = mCurrentFilter.getTable();
+ currentTable.selectAll();
+ }
+
+ /**
+ * Sets a TableFocusListener which will be notified when one of the tables
+ * gets or loses focus.
+ *
+ * @param listener
+ */
+ public void setTableFocusListener(ITableFocusListener listener) {
+ // record the global listener, to make sure table created after
+ // this call will still be setup.
+ mGlobalListener = listener;
+
+ // now we setup the existing filters
+ for (LogFilter filter : mFilters) {
+ Table table = filter.getTable();
+
+ addTableToFocusListener(table);
+ }
+
+ // and the default one
+ if (mDefaultFilter != null) {
+ addTableToFocusListener(mDefaultFilter.getTable());
+ }
+ }
+
+ /**
+ * Sets up a Table object to notify the global Table Focus listener when it
+ * gets or loses the focus.
+ *
+ * @param table the Table object.
+ */
+ private void addTableToFocusListener(final Table table) {
+ // create the activator for this table
+ final IFocusedTableActivator activator = new IFocusedTableActivator() {
+ public void copy(Clipboard clipboard) {
+ copyTable(clipboard, table);
+ }
+
+ public void selectAll() {
+ table.selectAll();
+ }
+ };
+
+ // add the focus listener on the table to notify the global listener
+ table.addFocusListener(new FocusListener() {
+ public void focusGained(FocusEvent e) {
+ mGlobalListener.focusGained(activator);
+ }
+
+ public void focusLost(FocusEvent e) {
+ mGlobalListener.focusLost(activator);
+ }
+ });
+ }
+
+ /**
+ * Copies the current selection of a Table into the provided Clipboard, as
+ * multi-line text.
+ *
+ * @param clipboard The clipboard to place the copied content.
+ * @param table The table to copy from.
+ */
+ private static void copyTable(Clipboard clipboard, Table table) {
+ int[] selection = table.getSelectionIndices();
+
+ // we need to sort the items to be sure.
+ Arrays.sort(selection);
+
+ // all lines must be concatenated.
+ StringBuilder sb = new StringBuilder();
+
+ // loop on the selection and output the file.
+ for (int i : selection) {
+ TableItem item = table.getItem(i);
+ LogMessage msg = (LogMessage)item.getData();
+ String line = msg.toString();
+ sb.append(line);
+ sb.append('\n');
+ }
+
+ // now add that to the clipboard
+ clipboard.setContents(new Object[] {
+ sb.toString()
+ }, new Transfer[] {
+ TextTransfer.getInstance()
+ });
+ }
+
+ /**
+ * Sets the log level for the current filter, but does not save it.
+ * @param i
+ */
+ public void setCurrentFilterLogLevel(int i) {
+ LogFilter filter = getCurrentFilter();
+
+ filter.setLogLevel(i);
+
+ initFilter(filter);
+ }
+
+ /**
+ * Creates a new tab in the folderTab item. Must be called from the ui
+ * thread.
+ * @param filter The filter associated with the tab.
+ * @param index the index of the tab. if -1, the tab will be added at the
+ * end.
+ * @param fillTable If true the table is filled with the current content of
+ * the buffer.
+ * @return The TabItem object that was created.
+ */
+ private TabItem createTab(LogFilter filter, int index, boolean fillTable) {
+ synchronized (mBuffer) {
+ TabItem item = null;
+ if (index != -1) {
+ item = new TabItem(mFolders, SWT.NONE, index);
+ } else {
+ item = new TabItem(mFolders, SWT.NONE);
+ }
+ item.setText(filter.getName());
+
+ // set the control (the parent is the TabFolder item, always)
+ Composite top = new Composite(mFolders, SWT.NONE);
+ item.setControl(top);
+
+ top.setLayout(new FillLayout());
+
+ // create the ui, first the table
+ final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+
+ if (mDisplayFont != null) {
+ t.setFont(mDisplayFont);
+ }
+
+ // give the ui objects to the filters.
+ filter.setWidgets(item, t);
+
+ t.setHeaderVisible(true);
+ t.setLinesVisible(false);
+
+ if (mGlobalListener != null) {
+ addTableToFocusListener(t);
+ }
+
+ // create a controllistener that will handle the resizing of all the
+ // columns (except the last) and of the table itself.
+ ControlListener listener = null;
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ listener = new ControlListener() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ public void controlResized(ControlEvent e) {
+ Rectangle r = t.getClientArea();
+
+ // get the size of all but the last column
+ int total = t.getColumn(0).getWidth();
+ total += t.getColumn(1).getWidth();
+ total += t.getColumn(2).getWidth();
+ total += t.getColumn(3).getWidth();
+
+ if (r.width > total) {
+ t.getColumn(4).setWidth(r.width-total);
+ }
+ }
+ };
+
+ t.addControlListener(listener);
+ }
+
+ // then its column
+ TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT,
+ "00-00 00:00:00", //$NON-NLS-1$
+ PREFS_TIME, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "", SWT.CENTER,
+ "D", //$NON-NLS-1$
+ PREFS_LEVEL, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "pid", SWT.LEFT,
+ "9999", //$NON-NLS-1$
+ PREFS_PID, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "tag", SWT.LEFT,
+ "abcdefgh", //$NON-NLS-1$
+ PREFS_TAG, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ col.addControlListener(listener);
+ }
+
+ col = TableHelper.createTableColumn(t, "Message", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$
+ PREFS_MESSAGE, mStore);
+ if (mColumnMode == COLUMN_MODE_AUTO) {
+ // instead of listening on resize for the last column, we make
+ // it non resizable.
+ col.setResizable(false);
+ }
+
+ if (fillTable) {
+ initFilter(filter);
+ }
+ return item;
+ }
+ }
+
+ protected void updateColumns(Table table) {
+ if (table != null) {
+ int index = 0;
+ TableColumn col;
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_TIME));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_LEVEL));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_PID));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_TAG));
+
+ col = table.getColumn(index++);
+ col.setWidth(mStore.getInt(PREFS_MESSAGE));
+ }
+ }
+
+ public void resetUI(boolean inUiThread) {
+ if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+ if (inUiThread) {
+ mFolders.dispose();
+ mParent.pack(true);
+ createControl(mParent);
+ } else {
+ Display d = mFolders.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ public void run() {
+ mFolders.dispose();
+ mParent.pack(true);
+ createControl(mParent);
+ }
+ });
+ }
+ } else {
+ // the ui is static we just empty it.
+ if (mFolders.isDisposed() == false) {
+ if (inUiThread) {
+ emptyTables();
+ } else {
+ Display d = mFolders.getDisplay();
+
+ // run sync as we need to update right now.
+ d.syncExec(new Runnable() {
+ public void run() {
+ if (mFolders.isDisposed() == false) {
+ emptyTables();
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Process new Log lines coming from {@link LogCatOuputReceiver}.
+ * @param lines the new lines
+ */
+ protected void processLogLines(String[] lines) {
+ // WARNING: this will not work if the string contains more line than
+ // the buffer holds.
+
+ if (lines.length > STRING_BUFFER_LENGTH) {
+ Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH");
+ }
+
+ // parse the lines and create LogMessage that are stored in a temporary list
+ final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>();
+
+ synchronized (mBuffer) {
+ for (String line : lines) {
+ // ignore empty lines.
+ if (line.length() > 0) {
+ // check for header lines.
+ Matcher matcher = sLogPattern.matcher(line);
+ if (matcher.matches()) {
+ // this is a header line, parse the header and keep it around.
+ mLastMessageInfo = new LogMessageInfo();
+
+ mLastMessageInfo.time = matcher.group(1);
+ mLastMessageInfo.pidString = matcher.group(2);
+ mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString);
+ mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4));
+ mLastMessageInfo.tag = matcher.group(5).trim();
+ } else {
+ // This is not a header line.
+ // Create a new LogMessage and process it.
+ LogMessage mc = new LogMessage();
+
+ if (mLastMessageInfo == null) {
+ // The first line of output wasn't preceded
+ // by a header line; make something up so
+ // that users of mc.data don't NPE.
+ mLastMessageInfo = new LogMessageInfo();
+ mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$
+ mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$
+ mLastMessageInfo.pid = 0;
+ mLastMessageInfo.logLevel = LogLevel.INFO;
+ mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$
+ }
+
+ // If someone printed a log message with
+ // embedded '\n' characters, there will
+ // one header line followed by multiple text lines.
+ // Use the last header that we saw.
+ mc.data = mLastMessageInfo;
+
+ // tabs seem to display as only 1 tab so we replace the leading tabs
+ // by 4 spaces.
+ mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // process the new LogMessage.
+ processNewMessage(mc);
+
+ // store the new LogMessage
+ newMessages.add(mc);
+ }
+ }
+ }
+
+ // if we don't have a pending Runnable that will do the refresh, we ask the Display
+ // to run one in the UI thread.
+ if (mPendingAsyncRefresh == false) {
+ mPendingAsyncRefresh = true;
+
+ try {
+ Display display = mFolders.getDisplay();
+
+ // run in sync because this will update the buffer start/end indices
+ display.asyncExec(new Runnable() {
+ public void run() {
+ asyncRefresh();
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed, we're probably quitting. Let's stop.
+ stopLogCat(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Refreshes the UI with new messages.
+ */
+ private void asyncRefresh() {
+ if (mFolders.isDisposed() == false) {
+ synchronized (mBuffer) {
+ try {
+ // the circular buffer has been updated, let have the filter flush their
+ // display with the new messages.
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ f.flush();
+ }
+ }
+
+ if (mDefaultFilter != null) {
+ mDefaultFilter.flush();
+ }
+ } finally {
+ // the pending refresh is done.
+ mPendingAsyncRefresh = false;
+ }
+ }
+ } else {
+ stopLogCat(true);
+ }
+ }
+
+ /**
+ * Processes a new Message.
+ * <p/>This adds the new message to the buffer, and gives it to the existing filters.
+ * @param newMessage
+ */
+ private void processNewMessage(LogMessage newMessage) {
+ // if we are in auto filtering mode, make sure we have
+ // a filter for this
+ if (mFilterMode == FILTER_AUTO_PID ||
+ mFilterMode == FILTER_AUTO_TAG) {
+ checkFilter(newMessage.data);
+ }
+
+ // compute the index where the message goes.
+ // was the buffer empty?
+ int messageIndex = -1;
+ if (mBufferStart == -1) {
+ messageIndex = mBufferStart = 0;
+ mBufferEnd = 1;
+ } else {
+ messageIndex = mBufferEnd;
+
+ // check we aren't overwriting start
+ if (mBufferEnd == mBufferStart) {
+ mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
+ }
+
+ // increment the next usable slot index
+ mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH;
+ }
+
+ LogMessage oldMessage = null;
+
+ // record the message that was there before
+ if (mBuffer[messageIndex] != null) {
+ oldMessage = mBuffer[messageIndex];
+ }
+
+ // then add the new one
+ mBuffer[messageIndex] = newMessage;
+
+ // give the new message to every filters.
+ boolean filtered = false;
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ filtered |= f.addMessage(newMessage, oldMessage);
+ }
+ }
+ if (filtered == false && mDefaultFilter != null) {
+ mDefaultFilter.addMessage(newMessage, oldMessage);
+ }
+ }
+
+ private void createFilters() {
+ if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) {
+ // unarchive the filters.
+ mFilters = mFilterStorage.getFilterFromStore();
+
+ // set the colors
+ if (mFilters != null) {
+ for (LogFilter f : mFilters) {
+ f.setColors(mColors);
+ }
+ }
+
+ if (mFilterStorage.requiresDefaultFilter()) {
+ mDefaultFilter = new LogFilter("Log");
+ mDefaultFilter.setColors(mColors);
+ mDefaultFilter.setSupportsDelete(false);
+ mDefaultFilter.setSupportsEdit(false);
+ }
+ } else if (mFilterMode == FILTER_NONE) {
+ // if the filtering mode is "none", we create a single filter that
+ // will receive all
+ mDefaultFilter = new LogFilter("Log");
+ mDefaultFilter.setColors(mColors);
+ mDefaultFilter.setSupportsDelete(false);
+ mDefaultFilter.setSupportsEdit(false);
+ }
+ }
+
+ /** Checks if there's an automatic filter for this md and if not
+ * adds the filter and the ui.
+ * This must be called from the UI!
+ * @param md
+ * @return true if the filter existed already
+ */
+ private boolean checkFilter(final LogMessageInfo md) {
+ if (true)
+ return true;
+ // look for a filter that matches the pid
+ if (mFilterMode == FILTER_AUTO_PID) {
+ for (LogFilter f : mFilters) {
+ if (f.getPidFilter() == md.pid) {
+ return true;
+ }
+ }
+ } else if (mFilterMode == FILTER_AUTO_TAG) {
+ for (LogFilter f : mFilters) {
+ if (f.getTagFilter().equals(md.tag)) {
+ return true;
+ }
+ }
+ }
+
+ // if we reach this point, no filter was found.
+ // create a filter with a temporary name of the pid
+ final LogFilter newFilter = new LogFilter(md.pidString);
+ String name = null;
+ if (mFilterMode == FILTER_AUTO_PID) {
+ newFilter.setPidMode(md.pid);
+
+ // ask the monitor thread if it knows the pid.
+ name = mCurrentLoggedDevice.getClientName(md.pid);
+ } else {
+ newFilter.setTagMode(md.tag);
+ name = md.tag;
+ }
+ addFilterToArray(newFilter);
+
+ final String fname = name;
+
+ // create the tabitem
+ final TabItem newTabItem = createTab(newFilter, -1, true);
+
+ // if the name is unknown
+ if (fname == null) {
+ // we need to find the process running under that pid.
+ // launch a thread do a ps on the device
+ new Thread("remote PS") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create the receiver
+ PsOutputReceiver psor = new PsOutputReceiver(md.pid,
+ newFilter, newTabItem);
+
+ // execute ps
+ try {
+ mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$
+ } catch (IOException e) {
+ // hmm...
+ }
+ }
+ }.start();
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a new filter to the current filter array, and set its colors
+ * @param newFilter The filter to add
+ */
+ private void addFilterToArray(LogFilter newFilter) {
+ // set the colors
+ newFilter.setColors(mColors);
+
+ // add it to the array.
+ if (mFilters != null && mFilters.length > 0) {
+ LogFilter[] newFilters = new LogFilter[mFilters.length+1];
+ System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length);
+ newFilters[mFilters.length] = newFilter;
+ mFilters = newFilters;
+ } else {
+ mFilters = new LogFilter[1];
+ mFilters[0] = newFilter;
+ }
+ }
+
+ private void removeFilterFromArray(LogFilter oldFilter) {
+ // look for the index
+ int index = -1;
+ for (int i = 0 ; i < mFilters.length ; i++) {
+ if (mFilters[i] == oldFilter) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index != -1) {
+ LogFilter[] newFilters = new LogFilter[mFilters.length-1];
+ System.arraycopy(mFilters, 0, newFilters, 0, index);
+ System.arraycopy(mFilters, index + 1, newFilters, index,
+ newFilters.length-index);
+ mFilters = newFilters;
+ }
+ }
+
+ /**
+ * Initialize the filter with already existing buffer.
+ * @param filter
+ */
+ private void initFilter(LogFilter filter) {
+ // is it empty
+ if (filter.uiReady() == false) {
+ return;
+ }
+
+ if (filter == mDefaultFilter) {
+ initDefaultFilter();
+ return;
+ }
+
+ filter.clear();
+
+ if (mBufferStart != -1) {
+ int max = mBufferEnd;
+ if (mBufferEnd < mBufferStart) {
+ max += STRING_BUFFER_LENGTH;
+ }
+
+ for (int i = mBufferStart; i < max; i++) {
+ int realItemIndex = i % STRING_BUFFER_LENGTH;
+
+ filter.addMessage(mBuffer[realItemIndex], null /* old message */);
+ }
+ }
+
+ filter.flush();
+ filter.resetTempFilteringStatus();
+ }
+
+ /**
+ * Refill the default filter. Not to be called directly.
+ * @see initFilter()
+ */
+ private void initDefaultFilter() {
+ mDefaultFilter.clear();
+
+ if (mBufferStart != -1) {
+ int max = mBufferEnd;
+ if (mBufferEnd < mBufferStart) {
+ max += STRING_BUFFER_LENGTH;
+ }
+
+ for (int i = mBufferStart; i < max; i++) {
+ int realItemIndex = i % STRING_BUFFER_LENGTH;
+ LogMessage msg = mBuffer[realItemIndex];
+
+ // first we check that the other filters don't take this message
+ boolean filtered = false;
+ for (LogFilter f : mFilters) {
+ filtered |= f.accept(msg);
+ }
+
+ if (filtered == false) {
+ mDefaultFilter.addMessage(msg, null /* old message */);
+ }
+ }
+ }
+
+ mDefaultFilter.flush();
+ mDefaultFilter.resetTempFilteringStatus();
+ }
+
+ /**
+ * Reset the filters, to handle change in device in automatic filter mode
+ */
+ private void resetFilters() {
+ // if we are in automatic mode, then we need to rmove the current
+ // filter.
+ if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+ mFilters = null;
+
+ // recreate the filters.
+ createFilters();
+ }
+ }
+
+
+ private LogFilter getCurrentFilter() {
+ int index = mFolders.getSelectionIndex();
+
+ // if mFilters is null or index is invalid, we return the default
+ // filter. It doesn't matter if that one is null as well, since we
+ // would return null anyway.
+ if (index == 0 || mFilters == null) {
+ return mDefaultFilter;
+ }
+
+ return mFilters[index-1];
+ }
+
+
+ private void emptyTables() {
+ for (LogFilter f : mFilters) {
+ f.getTable().removeAll();
+ }
+
+ if (mDefaultFilter != null) {
+ mDefaultFilter.getTable().removeAll();
+ }
+ }
+
+ protected void updateFilteringWith(String text) {
+ synchronized (mBuffer) {
+ // reset the temp filtering for all the filters
+ for (LogFilter f : mFilters) {
+ f.resetTempFiltering();
+ }
+ if (mDefaultFilter != null) {
+ mDefaultFilter.resetTempFiltering();
+ }
+
+ // now we need to figure out the new temp filtering
+ // split each word
+ String[] segments = text.split(" "); //$NON-NLS-1$
+
+ ArrayList<String> keywords = new ArrayList<String>(segments.length);
+
+ // loop and look for temp id/tag
+ int tempPid = -1;
+ String tempTag = null;
+ for (int i = 0 ; i < segments.length; i++) {
+ String s = segments[i];
+ if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$
+ // get the pid
+ String[] seg = s.split(":"); //$NON-NLS-1$
+ if (seg.length == 2) {
+ if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$
+ tempPid = Integer.valueOf(seg[1]);
+ }
+ }
+ } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$
+ String seg[] = segments[i].split(":"); //$NON-NLS-1$
+ if (seg.length == 2) {
+ tempTag = seg[1];
+ }
+ } else {
+ keywords.add(s);
+ }
+ }
+
+ // set the temp filtering in the filters
+ if (tempPid != -1 || tempTag != null || keywords.size() > 0) {
+ String[] keywordsArray = keywords.toArray(
+ new String[keywords.size()]);
+
+ for (LogFilter f : mFilters) {
+ if (tempPid != -1) {
+ f.setTempPidFiltering(tempPid);
+ }
+ if (tempTag != null) {
+ f.setTempTagFiltering(tempTag);
+ }
+ f.setTempKeywordFiltering(keywordsArray);
+ }
+
+ if (mDefaultFilter != null) {
+ if (tempPid != -1) {
+ mDefaultFilter.setTempPidFiltering(tempPid);
+ }
+ if (tempTag != null) {
+ mDefaultFilter.setTempTagFiltering(tempTag);
+ }
+ mDefaultFilter.setTempKeywordFiltering(keywordsArray);
+
+ }
+ }
+
+ initFilter(mCurrentFilter);
+ }
+ }
+
+ /**
+ * Called when the current filter selection changes.
+ * @param selectedFilter
+ */
+ private void selectionChanged(LogFilter selectedFilter) {
+ if (mLogLevelActions != null) {
+ // get the log level
+ int level = selectedFilter.getLogLevel();
+ for (int i = 0 ; i < mLogLevelActions.length; i++) {
+ ICommonAction a = mLogLevelActions[i];
+ if (i == level - 2) {
+ a.setChecked(true);
+ } else {
+ a.setChecked(false);
+ }
+ }
+ }
+
+ if (mDeleteFilterAction != null) {
+ mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete());
+ }
+ if (mEditFilterAction != null) {
+ mEditFilterAction.setEnabled(selectedFilter.supportsEdit());
+ }
+ }
+}
diff --git a/ddms/libs/ddmuilib/src/resources/images/add.png b/ddms/libs/ddmuilib/src/resources/images/add.png
new file mode 100644
index 0000000..eefc2ca
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/add.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/android.png b/ddms/libs/ddmuilib/src/resources/images/android.png
new file mode 100644
index 0000000..3779d4d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/android.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/backward.png b/ddms/libs/ddmuilib/src/resources/images/backward.png
new file mode 100644
index 0000000..90a9713
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/backward.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/clear.png b/ddms/libs/ddmuilib/src/resources/images/clear.png
new file mode 100644
index 0000000..0009cf6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/clear.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/d.png b/ddms/libs/ddmuilib/src/resources/images/d.png
new file mode 100644
index 0000000..d45506e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/d.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-attach.png b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png
new file mode 100644
index 0000000..9b8a11c
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-error.png b/ddms/libs/ddmuilib/src/resources/images/debug-error.png
new file mode 100644
index 0000000..f22da1f
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-error.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-wait.png b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png
new file mode 100644
index 0000000..322be63
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/delete.png b/ddms/libs/ddmuilib/src/resources/images/delete.png
new file mode 100644
index 0000000..db5fab8
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/delete.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/device.png b/ddms/libs/ddmuilib/src/resources/images/device.png
new file mode 100644
index 0000000..7dbbbb6
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/device.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/down.png b/ddms/libs/ddmuilib/src/resources/images/down.png
new file mode 100644
index 0000000..f9426cb
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/down.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/e.png b/ddms/libs/ddmuilib/src/resources/images/e.png
new file mode 100644
index 0000000..dee7c97
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/e.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/edit.png b/ddms/libs/ddmuilib/src/resources/images/edit.png
new file mode 100644
index 0000000..b8f65bc
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/edit.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/empty.png b/ddms/libs/ddmuilib/src/resources/images/empty.png
new file mode 100644
index 0000000..f021542
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/empty.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/emulator.png b/ddms/libs/ddmuilib/src/resources/images/emulator.png
new file mode 100644
index 0000000..a718042
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/emulator.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/file.png b/ddms/libs/ddmuilib/src/resources/images/file.png
new file mode 100644
index 0000000..043a814
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/file.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/folder.png b/ddms/libs/ddmuilib/src/resources/images/folder.png
new file mode 100644
index 0000000..7e29b1a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/folder.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/forward.png b/ddms/libs/ddmuilib/src/resources/images/forward.png
new file mode 100644
index 0000000..a97a605
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/forward.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/gc.png b/ddms/libs/ddmuilib/src/resources/images/gc.png
new file mode 100644
index 0000000..5194806
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/gc.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/halt.png b/ddms/libs/ddmuilib/src/resources/images/halt.png
new file mode 100644
index 0000000..10e3720
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/halt.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/heap.png b/ddms/libs/ddmuilib/src/resources/images/heap.png
new file mode 100644
index 0000000..e3aa3f0
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/heap.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/i.png b/ddms/libs/ddmuilib/src/resources/images/i.png
new file mode 100644
index 0000000..98385c5
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/i.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/importBug.png b/ddms/libs/ddmuilib/src/resources/images/importBug.png
new file mode 100644
index 0000000..f5da179
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/importBug.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/load.png b/ddms/libs/ddmuilib/src/resources/images/load.png
new file mode 100644
index 0000000..9e7bf6e
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/load.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/pause.png b/ddms/libs/ddmuilib/src/resources/images/pause.png
new file mode 100644
index 0000000..19d286d
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/pause.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/play.png b/ddms/libs/ddmuilib/src/resources/images/play.png
new file mode 100644
index 0000000..d54f013
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/play.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/pull.png b/ddms/libs/ddmuilib/src/resources/images/pull.png
new file mode 100644
index 0000000..f48f1b1
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/pull.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/push.png b/ddms/libs/ddmuilib/src/resources/images/push.png
new file mode 100644
index 0000000..6222864
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/push.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/save.png b/ddms/libs/ddmuilib/src/resources/images/save.png
new file mode 100644
index 0000000..040ebda
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/save.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/thread.png b/ddms/libs/ddmuilib/src/resources/images/thread.png
new file mode 100644
index 0000000..ac839e8
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/thread.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/up.png b/ddms/libs/ddmuilib/src/resources/images/up.png
new file mode 100644
index 0000000..92edf5a
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/up.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/v.png b/ddms/libs/ddmuilib/src/resources/images/v.png
new file mode 100644
index 0000000..8044051
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/v.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/w.png b/ddms/libs/ddmuilib/src/resources/images/w.png
new file mode 100644
index 0000000..129d0f9
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/w.png
Binary files differ
diff --git a/ddms/libs/ddmuilib/src/resources/images/warning.png b/ddms/libs/ddmuilib/src/resources/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/resources/images/warning.png
Binary files differ