diff options
Diffstat (limited to 'ddms')
180 files changed, 39390 insertions, 0 deletions
diff --git a/ddms/Android.mk b/ddms/Android.mk new file mode 100644 index 0000000..82c248e --- /dev/null +++ b/ddms/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMS_LOCAL_DIR := $(call my-dir) +include $(DDMS_LOCAL_DIR)/libs/Android.mk +include $(DDMS_LOCAL_DIR)/app/Android.mk diff --git a/ddms/MODULE_LICENSE_APACHE2 b/ddms/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ddms/MODULE_LICENSE_APACHE2 diff --git a/ddms/app/.classpath b/ddms/app/.classpath new file mode 100644 index 0000000..2fa1fb7 --- /dev/null +++ b/ddms/app/.classpath @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="Makefile|resources/" kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmuilib"/> + <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkStatsService"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/app/.project b/ddms/app/.project new file mode 100644 index 0000000..ffb19d7 --- /dev/null +++ b/ddms/app/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddms</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/app/Android.mk b/ddms/app/Android.mk new file mode 100644 index 0000000..3857706 --- /dev/null +++ b/ddms/app/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMSAPP_LOCAL_DIR := $(call my-dir) +include $(DDMSAPP_LOCAL_DIR)/etc/Android.mk +include $(DDMSAPP_LOCAL_DIR)/src/Android.mk diff --git a/ddms/app/README b/ddms/app/README new file mode 100644 index 0000000..cc55ddd --- /dev/null +++ b/ddms/app/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for ddms. + +ddms requires SWT to compile. + +SWT is available in the depot under //device/prebuild/<platform>/swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at //device/prebuild/<platform>/swt.
\ No newline at end of file diff --git a/ddms/app/etc/Android.mk b/ddms/app/etc/Android.mk new file mode 100644 index 0000000..9d69971 --- /dev/null +++ b/ddms/app/etc/Android.mk @@ -0,0 +1,8 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := ddms +include $(BUILD_HOST_PREBUILT) + diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms new file mode 100755 index 0000000..d809cfc --- /dev/null +++ b/ddms/app/etc/ddms @@ -0,0 +1,84 @@ +#!/bin/sh +# Copyright 2005-2007, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=ddms.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" + #because Java 1.6 is 64 bits only and SWT doesn't support this, we force the usage of java 1.5 + java_cmd="/System/Library/Frameworks/JavaVM.framework/Versions/1.5/Commands/java" +else + os_opts= + java_cmd="java" +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec "$java_cmd" -Xmx256M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.ddms.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat new file mode 100755 index 0000000..5da9fb5 --- /dev/null +++ b/ddms/app/etc/ddms.bat @@ -0,0 +1,48 @@ +@echo off +rem Copyright (C) 2007 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +set jarfile=ddms.jar +set frameworkdir= +set libdir= + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=lib\ + set libdir=lib\ + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=..\framework\ + set libdir=..\lib\ + +:JarFileOk + +if debug NEQ "%1" goto NoDebug + set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +:NoDebug + +set jarpath=%frameworkdir%%jarfile% + +call java %java_debug% -Djava.ext.dirs=%frameworkdir% -Djava.library.path=%libdir% -Dcom.android.ddms.bindir= -jar %jarpath% %* diff --git a/ddms/app/etc/manifest.txt b/ddms/app/etc/manifest.txt new file mode 100644 index 0000000..84c8acd --- /dev/null +++ b/ddms/app/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.ddms.Main diff --git a/ddms/app/src/Android.mk b/ddms/app/src/Android.mk new file mode 100644 index 0000000..a013fa6 --- /dev/null +++ b/ddms/app/src/Android.mk @@ -0,0 +1,22 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + androidprefs \ + sdkstats \ + ddmlib \ + ddmuilib \ + swt \ + org.eclipse.jface_3.2.0.I20060605-1400 \ + org.eclipse.equinox.common_3.2.0.v20060603 \ + org.eclipse.core.commands_3.2.0.I20060605-1400 +LOCAL_MODULE := ddms + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/app/src/com/android/ddms/AboutDialog.java b/ddms/app/src/com/android/ddms/AboutDialog.java new file mode 100644 index 0000000..2910e5e --- /dev/null +++ b/ddms/app/src/com/android/ddms/AboutDialog.java @@ -0,0 +1,153 @@ +/* //device/tools/ddms/src/com/android/ddms/AboutDialog.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddms; + +import com.android.ddmlib.Log; +import com.android.ddmuilib.ImageHelper; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.InputStream; + +/** + * Our "about" box. + */ +public class AboutDialog extends Dialog { + + private Image logoImage; + + /** + * Create with default style. + */ + public AboutDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create with app-defined style. + */ + public AboutDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + */ + public void open() { + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("About..."); + + logoImage = loadImage(shell, "ddms-logo.png"); // $NON-NLS-1$ + createContents(shell); + shell.pack(); + + shell.open(); + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + logoImage.dispose(); + } + + /* + * Load an image file from a resource. + * + * This depends on Display, so I'm not sure what the rules are for + * loading once and caching in a static class field. + */ + private Image loadImage(Shell shell, String fileName) { + InputStream imageStream; + String pathName = "/images/" + fileName; // $NON-NLS-1$ + + imageStream = this.getClass().getResourceAsStream(pathName); + if (imageStream == null) { + //throw new NullPointerException("couldn't find " + pathName); + Log.w("ddms", "Couldn't load " + pathName); + Display display = shell.getDisplay(); + return ImageHelper.createPlaceHolderArt(display, 100, 50, + display.getSystemColor(SWT.COLOR_BLUE)); + } + + Image img = new Image(shell.getDisplay(), imageStream); + if (img == null) + throw new NullPointerException("couldn't load " + pathName); + return img; + } + + /* + * Create the about box contents. + */ + private void createContents(final Shell shell) { + GridLayout layout; + GridData data; + Label label; + + shell.setLayout(new GridLayout(2, false)); + + // Fancy logo + Label logo = new Label(shell, SWT.BORDER); + logo.setImage(logoImage); + + // Text Area + Composite textArea = new Composite(shell, SWT.NONE); + layout = new GridLayout(1, true); + textArea.setLayout(layout); + + // Text lines + label = new Label(textArea, SWT.NONE); + label.setText("Dalvik Debug Monitor v" + Main.VERSION); + label = new Label(textArea, SWT.NONE); + label.setText("Copyright 2007, The Android Open Source Project"); + label = new Label(textArea, SWT.NONE); + label.setText("All Rights Reserved."); + + // blank spot in grid + label = new Label(shell, SWT.NONE); + + // "OK" button + Button ok = new Button(shell, SWT.PUSH); + ok.setText("OK"); + data = new GridData(GridData.HORIZONTAL_ALIGN_END); + data.widthHint = 80; + ok.setLayoutData(data); + ok.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + shell.pack(); + + shell.setDefaultButton(ok); + } +} diff --git a/ddms/app/src/com/android/ddms/DebugPortProvider.java b/ddms/app/src/com/android/ddms/DebugPortProvider.java new file mode 100644 index 0000000..89cc190 --- /dev/null +++ b/ddms/app/src/com/android/ddms/DebugPortProvider.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddms; + +import com.android.ddmlib.Device; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * DDMS implementation of the IDebugPortProvider interface. + * This class handles saving/loading the list of static debug port from + * the preference store and provides the port number to the Device Monitor. + */ +public class DebugPortProvider implements IDebugPortProvider { + + private static DebugPortProvider sThis = new DebugPortProvider(); + + /** Preference name for the static port list. */ + public static final String PREFS_STATIC_PORT_LIST = "android.staticPortList"; //$NON-NLS-1$ + + /** + * Mapping device serial numbers to maps. The embedded maps are mapping application names to + * debugger ports. + */ + private Map<String, Map<String, Integer>> mMap; + + public static DebugPortProvider getInstance() { + return sThis; + } + + private DebugPortProvider() { + computePortList(); + } + + /** + * Returns a static debug port for the specified application running on the + * specified {@link Device}. + * @param device The device the application is running on. + * @param appName The application name, as defined in the + * AndroidManifest.xml package attribute. + * @return The static debug port or {@link #NO_STATIC_PORT} if there is none setup. + * + * @see IDebugPortProvider#getPort(Device, String) + */ + public int getPort(Device device, String appName) { + if (mMap != null) { + Map<String, Integer> deviceMap = mMap.get(device.getSerialNumber()); + if (deviceMap != null) { + Integer i = deviceMap.get(appName); + if (i != null) { + return i.intValue(); + } + } + } + return IDebugPortProvider.NO_STATIC_PORT; + } + + /** + * Returns the map of Static debugger ports. The map links device serial numbers to + * a map linking application name to debugger ports. + */ + public Map<String, Map<String, Integer>> getPortList() { + return mMap; + } + + /** + * Create the map member from the values contained in the Preference Store. + */ + private void computePortList() { + mMap = new HashMap<String, Map<String, Integer>>(); + + // get the prefs store + IPreferenceStore store = PrefsDialog.getStore(); + String value = store.getString(PREFS_STATIC_PORT_LIST); + + if (value != null && value.length() > 0) { + // format is + // port1|port2|port3|... + // where port# is + // appPackageName:appPortNumber:device-serial-number + String[] portSegments = value.split("\\|"); //$NON-NLS-1$ + for (String seg : portSegments) { + String[] entry = seg.split(":"); //$NON-NLS-1$ + + // backward compatibility support. if we have only 2 entry, we default + // to the first emulator. + String deviceName = null; + if (entry.length == 3) { + deviceName = entry[2]; + } else { + deviceName = Device.FIRST_EMULATOR_SN; + } + + // get the device map + Map<String, Integer> deviceMap = mMap.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap<String, Integer>(); + mMap.put(deviceName, deviceMap); + } + + deviceMap.put(entry[0], Integer.valueOf(entry[1])); + } + } + } + + /** + * Sets new [device, app, port] values. + * The values are also sync'ed in the preference store. + * @param map The map containing the new values. + */ + public void setPortList(Map<String, Map<String,Integer>> map) { + // update the member map. + mMap.clear(); + mMap.putAll(map); + + // create the value to store in the preference store. + // see format definition in getPortList + StringBuilder sb = new StringBuilder(); + + Set<String> deviceKeys = map.keySet(); + for (String deviceKey : deviceKeys) { + Map<String, Integer> deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set<String> appKeys = deviceMap.keySet(); + + for (String appKey : appKeys) { + Integer port = deviceMap.get(appKey); + if (port != null) { + sb.append(appKey).append(':').append(port.intValue()).append(':'). + append(deviceKey).append('|'); + } + } + } + } + + String value = sb.toString(); + + // get the prefs store. + IPreferenceStore store = PrefsDialog.getStore(); + + // and give it the new value. + store.setValue(PREFS_STATIC_PORT_LIST, value); + } +} diff --git a/ddms/app/src/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java new file mode 100644 index 0000000..2a1342e --- /dev/null +++ b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java @@ -0,0 +1,423 @@ +/* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddms; + +import com.android.ddmlib.Device; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + + +/** + * Execute a command on an ADB-attached device and save the output. + * + * There are several ways to do this. One is to run a single command + * and show the output. Another is to have several possible commands and + * let the user click a button next to the one (or ones) they want. This + * currently uses the simple 1:1 form. + */ +public class DeviceCommandDialog extends Dialog { + + public static final int DEVICE_STATE = 0; + public static final int APP_STATE = 1; + public static final int RADIO_STATE = 2; + public static final int LOGCAT = 3; + + private String mCommand; + private String mFileName; + + private Label mStatusLabel; + private Button mCancelDone; + private Button mSave; + private Text mText; + private Font mFont = null; + private boolean mCancel; + private boolean mFinished; + + + /** + * Create with default style. + */ + public DeviceCommandDialog(String command, String fileName, Shell parent) { + // don't want a close button, but it seems hard to get rid of on GTK + // keep it on all platforms for consistency + this(command, fileName, parent, + SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE); + } + + /** + * Create with app-defined style. + */ + public DeviceCommandDialog(String command, String fileName, Shell parent, + int style) + { + super(parent, style); + mCommand = command; + mFileName = fileName; + } + + /** + * Prepare and display the dialog. + * @param currentDevice + */ + public void open(Device currentDevice) { + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Remote Command"); + + mFinished = false; + mFont = findFont(shell.getDisplay()); + createContents(shell); + + // Getting weird layout behavior under Linux when Text is added -- + // looks like text widget has min width of 400 when FILL_HORIZONTAL + // is used, and layout gets tweaked to force this. (Might be even + // more with the scroll bars in place -- it wigged out when the + // file save dialog was invoked.) + shell.setMinimumSize(500, 200); + shell.setSize(800, 600); + shell.open(); + + executeCommand(shell, currentDevice); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + if (mFont != null) + mFont.dispose(); + } + + /* + * Create a text widget to show the output and some buttons to + * manage things. + */ + private void createContents(final Shell shell) { + GridData data; + + shell.setLayout(new GridLayout(2, true)); + + shell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + if (!mFinished) { + Log.i("ddms", "NOT closing - cancelling command"); + event.doit = false; + mCancel = true; + } + } + }); + + mStatusLabel = new Label(shell, SWT.NONE); + mStatusLabel.setText("Executing '" + shortCommandString() + "'"); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = 2; + mStatusLabel.setLayoutData(data); + + mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + mText.setEditable(false); + mText.setFont(mFont); + data = new GridData(GridData.FILL_BOTH); + data.horizontalSpan = 2; + mText.setLayoutData(data); + + // "save" button + mSave = new Button(shell, SWT.PUSH); + mSave.setText("Save"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mSave.setLayoutData(data); + mSave.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + saveText(shell); + } + }); + mSave.setEnabled(false); + + // "cancel/done" button + mCancelDone = new Button(shell, SWT.PUSH); + mCancelDone.setText("Cancel"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mCancelDone.setLayoutData(data); + mCancelDone.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (!mFinished) + mCancel = true; + else + shell.close(); + } + }); + } + + /* + * Figure out what font to use. + * + * Returns "null" if we can't figure it out, which SWT understands to + * mean "use default system font". + */ + private Font findFont(Display display) { + String fontStr = PrefsDialog.getStore().getString("textOutputFont"); + if (fontStr != null) { + FontData fdat = new FontData(fontStr); + if (fdat != null) + return new Font(display, fdat); + } + return null; + } + + + /* + * Callback class for command execution. + */ + class Gatherer extends Thread implements IShellOutputReceiver { + public static final int RESULT_UNKNOWN = 0; + public static final int RESULT_SUCCESS = 1; + public static final int RESULT_FAILURE = 2; + public static final int RESULT_CANCELLED = 3; + + private Shell mShell; + private String mCommand; + private Text mText; + private int mResult; + private Device mDevice; + + /** + * Constructor; pass in the text widget that will receive the output. + * @param device + */ + public Gatherer(Shell shell, Device device, String command, Text text) { + mShell = shell; + mDevice = device; + mCommand = command; + mText = text; + mResult = RESULT_UNKNOWN; + + // this is in outer class + mCancel = false; + } + + /** + * Thread entry point. + */ + @Override + public void run() { + + if (mDevice == null) { + Log.w("ddms", "Cannot execute command: no device selected."); + mResult = RESULT_FAILURE; + } else { + try { + mDevice.executeShellCommand(mCommand, this); + if (mCancel) + mResult = RESULT_CANCELLED; + else + mResult = RESULT_SUCCESS; + } + catch (IOException ioe) { + Log.w("ddms", "Remote exec failed: " + ioe.getMessage()); + mResult = RESULT_FAILURE; + } + } + + mShell.getDisplay().asyncExec(new Runnable() { + public void run() { + updateForResult(mResult); + } + }); + } + + /** + * Called by executeRemoteCommand(). + */ + public void addOutput(byte[] data, int offset, int length) { + + Log.v("ddms", "received " + length + " bytes"); + try { + final String text; + text = new String(data, offset, length, "ISO-8859-1"); + + // add to text widget; must do in UI thread + mText.getDisplay().asyncExec(new Runnable() { + public void run() { + mText.append(text); + } + }); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + } + } + + public void flush() { + // nothing to flush. + } + + /** + * Called by executeRemoteCommand(). + */ + public boolean isCancelled() { + return mCancel; + } + }; + + /* + * Execute a remote command, add the output to the text widget, and + * update controls. + * + * We have to run the command in a thread so that the UI continues + * to work. + */ + private void executeCommand(Shell shell, Device device) { + Gatherer gath = new Gatherer(shell, device, commandString(), mText); + gath.start(); + } + + /* + * Update the controls after the remote operation completes. This + * must be called from the UI thread. + */ + private void updateForResult(int result) { + if (result == Gatherer.RESULT_SUCCESS) { + mStatusLabel.setText("Successfully executed '" + + shortCommandString() + "'"); + mSave.setEnabled(true); + } else if (result == Gatherer.RESULT_CANCELLED) { + mStatusLabel.setText("Execution cancelled; partial results below"); + mSave.setEnabled(true); // save partial + } else if (result == Gatherer.RESULT_FAILURE) { + mStatusLabel.setText("Failed"); + } + mStatusLabel.pack(); + mCancelDone.setText("Done"); + mFinished = true; + } + + /* + * Allow the user to save the contents of the text dialog. + */ + private void saveText(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + String fileName; + + dlg.setText("Save output..."); + dlg.setFileName(defaultFileName()); + dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir")); + dlg.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + dlg.setFilterExtensions(new String[] { + "*.txt" + }); + + fileName = dlg.open(); + if (fileName != null) { + PrefsDialog.getStore().setValue("lastTextSaveDir", + dlg.getFilterPath()); + + Log.i("ddms", "Saving output to " + fileName); + + /* + * Convert to 8-bit characters. + */ + String text = mText.getText(); + byte[] ascii; + try { + ascii = text.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); + ascii = new byte[0]; + } + + /* + * Output data, converting CRLF to LF. + */ + try { + int length = ascii.length; + + FileOutputStream outFile = new FileOutputStream(fileName); + BufferedOutputStream out = new BufferedOutputStream(outFile); + for (int i = 0; i < length; i++) { + if (i < length-1 && + ascii[i] == 0x0d && ascii[i+1] == 0x0a) + { + continue; + } + out.write(ascii[i]); + } + out.close(); // flush buffer, close file + } + catch (IOException ioe) { + Log.w("ddms", "Unable to save " + fileName + ": " + ioe); + } + } + } + + + /* + * Return the shell command we're going to use. + */ + private String commandString() { + return mCommand; + + } + + /* + * Return a default filename for the "save" command. + */ + private String defaultFileName() { + return mFileName; + } + + /* + * Like commandString(), but length-limited. + */ + private String shortCommandString() { + String str = commandString(); + if (str.length() > 50) + return str.substring(0, 50) + "..."; + else + return str; + } +} + diff --git a/ddms/app/src/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/com/android/ddms/DropdownSelectionListener.java new file mode 100644 index 0000000..99d63ce --- /dev/null +++ b/ddms/app/src/com/android/ddms/DropdownSelectionListener.java @@ -0,0 +1,80 @@ +/* //device/tools/ddms/src/com/android/ddms/DropdownSelectionListener.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddms; + +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; + +/** + * Helper class for drop-down menus in toolbars. + */ +public class DropdownSelectionListener extends SelectionAdapter { + private Menu mMenu; + private ToolItem mDropdown; + + /** + * Basic constructor. Creates an empty Menu to hold items. + */ + public DropdownSelectionListener(ToolItem item) { + mDropdown = item; + mMenu = new Menu(item.getParent().getShell(), SWT.POP_UP); + } + + /** + * Add an item to the dropdown menu. + */ + public void add(String label) { + MenuItem item = new MenuItem(mMenu, SWT.NONE); + item.setText(label); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // update the dropdown's text to match the selection + MenuItem sel = (MenuItem) e.widget; + mDropdown.setText(sel.getText()); + } + }); + } + + /** + * Invoked when dropdown or neighboring arrow is clicked. + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (e.detail == SWT.ARROW) { + // arrow clicked, show menu + ToolItem item = (ToolItem) e.widget; + Rectangle rect = item.getBounds(); + Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y)); + mMenu.setLocation(pt.x, pt.y + rect.height); + mMenu.setVisible(true); + } else { + // button clicked + Log.i("ddms", mDropdown.getText() + " Pressed"); + } + } +} + diff --git a/ddms/app/src/com/android/ddms/Main.java b/ddms/app/src/com/android/ddms/Main.java new file mode 100644 index 0000000..d63b884 --- /dev/null +++ b/ddms/app/src/com/android/ddms/Main.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddms; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.DebugPortManager; +import com.android.ddmlib.Log; +import com.android.sdkstats.SdkStatsService; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; + + +/** + * Start the UI and network. + */ +public class Main { + + /** User visible version number. */ + public static final String VERSION = "0.8.1"; + + public Main() { + } + + /* + * If a thread bails with an uncaught exception, bring the whole + * thing down. + */ + private static class UncaughtHandler implements Thread.UncaughtExceptionHandler { + public void uncaughtException(Thread t, Throwable e) { + Log.e("ddms", "shutting down due to uncaught exception"); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + Log.e("ddms", sw.toString()); + + System.exit(1); + } + } + + /** + * Parse args, start threads. + */ + public static void main(String[] args) { + // In order to have the AWT/SWT bridge work on Leopard, we do this little hack. + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + RuntimeMXBean rt = ManagementFactory.getRuntimeMXBean(); + System.setProperty( + "JAVA_STARTED_ON_FIRST_THREAD_" + (rt.getName().split("@"))[0], //$NON-NLS-1$ + "1"); //$NON-NLS-1$ + } + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); + + // load prefs and init the default values + PrefsDialog.init(); + + Log.d("ddms", "Initializing"); + + // the "ping" argument means to check in with the server and exit + // the application name and version number must also be supplied + if (args.length >= 3 && args[0].equals("ping")) { + SdkStatsService.ping(args[1], args[2]); + return; + } else if (args.length > 0) { + Log.e("ddms", "Unknown argument: " + args[0]); + System.exit(1); + } + + // ddms itself is wanted: send a ping for ourselves + SdkStatsService.ping("ddms", VERSION); //$NON-NLS-1$ + + DebugPortManager.setProvider(DebugPortProvider.getInstance()); + + // create the three main threads + UIThread ui = UIThread.getInstance(); + + try { + ui.runUI(); + } finally { + PrefsDialog.save(); + + AndroidDebugBridge.terminate(); + } + + Log.d("ddms", "Bye"); + + // this is kinda bad, but on MacOS the shutdown doesn't seem to finish because of + // a thread called AWT-Shutdown. This will help while I track this down. + System.exit(0); + } +} diff --git a/ddms/app/src/com/android/ddms/PrefsDialog.java b/ddms/app/src/com/android/ddms/PrefsDialog.java new file mode 100644 index 0000000..69c48b0 --- /dev/null +++ b/ddms/app/src/com/android/ddms/PrefsDialog.java @@ -0,0 +1,545 @@ +/* //device/tools/ddms/src/com/android/ddms/PrefsDialog.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddms; + +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.PortFieldEditor; +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.DirectoryFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FontFieldEditor; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferenceManager; +import org.eclipse.jface.preference.PreferenceNode; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.jface.preference.RadioGroupFieldEditor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.IOException; + +/** + * Preferences dialog. + */ +public final class PrefsDialog { + + // Preference store. + private static PreferenceStore mPrefStore; + + // public const values for storage + public final static String SHELL_X = "shellX"; //$NON-NLS-1$ + public final static String SHELL_Y = "shellY"; //$NON-NLS-1$ + public final static String SHELL_WIDTH = "shellWidth"; //$NON-NLS-1$ + public final static String SHELL_HEIGHT = "shellHeight"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_X = "explorerShellX"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_Y = "explorerShellY"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_WIDTH = "explorerShellWidth"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_HEIGHT = "explorerShellHeight"; //$NON-NLS-1$ + public final static String SHOW_NATIVE_HEAP = "native"; //$NON-NLS-1$ + + public final static String LOGCAT_COLUMN_MODE = "ddmsLogColumnMode"; //$NON-NLS-1$ + public final static String LOGCAT_FONT = "ddmsLogFont"; //$NON-NLS-1$ + + public final static String LOGCAT_COLUMN_MODE_AUTO = "auto"; //$NON-NLS-1$ + public final static String LOGCAT_COLUMN_MODE_MANUAL = "manual"; //$NON-NLS-1$ + + private final static String PREFS_DEBUG_PORT_BASE = "adbDebugBasePort"; //$NON-NLS-1$ + private final static String PREFS_SELECTED_DEBUG_PORT = "debugSelectedPort"; //$NON-NLS-1$ + private final static String PREFS_DEFAULT_THREAD_UPDATE = "defaultThreadUpdateEnabled"; //$NON-NLS-1$ + private final static String PREFS_DEFAULT_HEAP_UPDATE = "defaultHeapUpdateEnabled"; //$NON-NLS-1$ + + private final static String PREFS_THREAD_REFRESH_INTERVAL = "threadStatusInterval"; //$NON-NLS-1$ + + private final static String PREFS_LOG_LEVEL = "ddmsLogLevel"; //$NON-NLS-1$ + + + /** + * Private constructor -- do not instantiate. + */ + private PrefsDialog() {} + + /** + * Return the PreferenceStore that holds our values. + */ + public static PreferenceStore getStore() { + return mPrefStore; + } + + /** + * Save the prefs to the config file. + */ + public static void save() { + try { + mPrefStore.save(); + } + catch (IOException ioe) { + Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); + } + } + + /** + * Do some one-time prep. + * + * The original plan was to let the individual classes define their + * own defaults, which we would get and then override with the config + * file. However, PreferencesStore.load() doesn't trigger the "changed" + * events, which means we have to pull the loaded config values out by + * hand. + * + * So, we set the defaults, load the values from the config file, and + * then run through and manually export the values. Then we duplicate + * the second part later on for the "changed" events. + */ + public static void init() { + assert mPrefStore == null; + + mPrefStore = SdkStatsService.getPreferenceStore(); + + if (mPrefStore == null) { + // we have a serious issue here... + Log.e("ddms", + "failed to access both the user HOME directory and the system wide temp folder. Quitting."); + System.exit(1); + } + + // configure default values + setDefaults(System.getProperty("user.home")); //$NON-NLS-1$ + + // listen for changes + mPrefStore.addPropertyChangeListener(new ChangeListener()); + + // Now we initialize the value of the preference, from the values in the store. + + // First the ddm lib. + DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE)); + DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); + DdmPreferences.setLogLevel(mPrefStore.getString(PREFS_LOG_LEVEL)); + DdmPreferences.setInitialThreadUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE)); + DdmPreferences.setInitialHeapUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE)); + + // some static values + String out = System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ + DdmUiPreferences.setSymbolsLocation(out + File.separator + "symbols"); //$NON-NLS-1$ + DdmUiPreferences.setAddr2LineLocation("arm-eabi-addr2line"); //$NON-NLS-1$ + + String traceview = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ + if (traceview != null && traceview.length() != 0) { + traceview += File.separator + "traceview"; //$NON-NLS-1$ + } else { + traceview = "traceview"; //$NON-NLS-1$ + } + DdmUiPreferences.setTraceviewLocation(traceview); + + // Now the ddmui lib + DdmUiPreferences.setStore(mPrefStore); + DdmUiPreferences.setThreadRefreshInterval(mPrefStore.getInt(PREFS_THREAD_REFRESH_INTERVAL)); + } + + /* + * Set default values for all preferences. These are either defined + * statically or are based on the values set by the class initializers + * in other classes. + * + * The other threads (e.g. VMWatcherThread) haven't been created yet, + * so we want to use static values rather than reading fields from + * class.getInstance(). + */ + private static void setDefaults(String homeDir) { + mPrefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE); + + mPrefStore.setDefault(PREFS_SELECTED_DEBUG_PORT, + DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT); + + mPrefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true); + mPrefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false); + mPrefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL, + DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL); + + mPrefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$ + mPrefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$ + + mPrefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$ + + // choose a default font for the text output + FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$ + mPrefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$ + + // layout information. + mPrefStore.setDefault(SHELL_X, 100); + mPrefStore.setDefault(SHELL_Y, 100); + mPrefStore.setDefault(SHELL_WIDTH, 800); + mPrefStore.setDefault(SHELL_HEIGHT, 600); + + mPrefStore.setDefault(EXPLORER_SHELL_X, 50); + mPrefStore.setDefault(EXPLORER_SHELL_Y, 50); + + mPrefStore.setDefault(SHOW_NATIVE_HEAP, false); + } + + + /* + * Create a "listener" to take action when preferences change. These are + * required for ongoing activities that don't check prefs on each use. + * + * This is only invoked when something explicitly changes the value of + * a preference (e.g. not when the prefs file is loaded). + */ + private static class ChangeListener implements IPropertyChangeListener { + public void propertyChange(PropertyChangeEvent event) { + String changed = event.getProperty(); + + if (changed.equals(PREFS_DEBUG_PORT_BASE)) { + DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE)); + } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) { + DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); + } else if (changed.equals(PREFS_LOG_LEVEL)) { + DdmPreferences.setLogLevel((String)event.getNewValue()); + } else if (changed.equals("textSaveDir")) { + mPrefStore.setValue("lastTextSaveDir", + (String) event.getNewValue()); + } else if (changed.equals("imageSaveDir")) { + mPrefStore.setValue("lastImageSaveDir", + (String) event.getNewValue()); + + } else { + Log.v("ddms", "Preference change: " + event.getProperty() + + ": '" + event.getOldValue() + + "' --> '" + event.getNewValue() + "'"); + } + } + } + + + /** + * Create and display the dialog. + */ + public static void run(Shell shell) { + assert mPrefStore != null; + + PreferenceManager prefMgr = new PreferenceManager(); + + PreferenceNode node, subNode; + + // this didn't work -- got NPE, possibly from class lookup: + //PreferenceNode app = new PreferenceNode("app", "Application", null, + // AppPrefs.class.getName()); + + node = new PreferenceNode("client", new ClientPrefs()); + prefMgr.addToRoot(node); + + subNode = new PreferenceNode("panel", new PanelPrefs()); + //prefMgr.addTo(node.getId(), subNode); + prefMgr.addToRoot(subNode); + + node = new PreferenceNode("device", new DevicePrefs()); + prefMgr.addToRoot(node); + + node = new PreferenceNode("LogCat", new LogCatPrefs()); + prefMgr.addToRoot(node); + + node = new PreferenceNode("app", new AppPrefs()); + prefMgr.addToRoot(node); + + node = new PreferenceNode("stats", new UsageStatsPrefs()); + prefMgr.addToRoot(node); + + PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr); + dlg.setPreferenceStore(mPrefStore); + + // run it + dlg.open(); + + // save prefs + try { + mPrefStore.save(); + } + catch (IOException ioe) { + } + + // discard the stuff we created + //prefMgr.dispose(); + //dlg.dispose(); + } + + /** + * "Client Scan" prefs page. + */ + private static class ClientPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public ClientPrefs() { + super(GRID); // use "grid" layout so edit boxes line up + setTitle("Client Scan"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + IntegerFieldEditor ife; + + ife = new PortFieldEditor(PREFS_DEBUG_PORT_BASE, + "ADB debugger base:", getFieldEditorParent()); + addField(ife); + + ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT, + "Debug selected VM:", getFieldEditorParent()); + addField(ife); + } + } + + /** + * "Panel" prefs page. + */ + private static class PanelPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public PanelPrefs() { + super(FLAT); // use "flat" layout + setTitle("Info Panels"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + BooleanFieldEditor bfe; + IntegerFieldEditor ife; + + bfe = new BooleanFieldEditor(PREFS_DEFAULT_THREAD_UPDATE, + "Thread updates enabled by default", getFieldEditorParent()); + addField(bfe); + + bfe = new BooleanFieldEditor(PREFS_DEFAULT_HEAP_UPDATE, + "Heap updates enabled by default", getFieldEditorParent()); + addField(bfe); + + ife = new IntegerFieldEditor(PREFS_THREAD_REFRESH_INTERVAL, + "Thread status interval (seconds):", getFieldEditorParent()); + ife.setValidRange(1, 60); + addField(ife); + } + } + + /** + * "Device" prefs page. + */ + private static class DevicePrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public DevicePrefs() { + super(FLAT); // use "flat" layout + setTitle("Device"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + DirectoryFieldEditor dfe; + FontFieldEditor ffe; + + dfe = new DirectoryFieldEditor("textSaveDir", + "Default text save dir:", getFieldEditorParent()); + addField(dfe); + + dfe = new DirectoryFieldEditor("imageSaveDir", + "Default image save dir:", getFieldEditorParent()); + addField(dfe); + + ffe = new FontFieldEditor("textOutputFont", "Text output font:", + getFieldEditorParent()); + addField(ffe); + } + } + + /** + * "logcat" prefs page. + */ + private static class LogCatPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public LogCatPrefs() { + super(FLAT); // use "flat" layout + setTitle("Logcat"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + RadioGroupFieldEditor rgfe; + + rgfe = new RadioGroupFieldEditor(PrefsDialog.LOGCAT_COLUMN_MODE, + "Message Column Resizing Mode", 1, new String[][] { + { "Manual", PrefsDialog.LOGCAT_COLUMN_MODE_MANUAL }, + { "Automatic", PrefsDialog.LOGCAT_COLUMN_MODE_AUTO }, + }, + getFieldEditorParent(), true); + addField(rgfe); + + FontFieldEditor ffe = new FontFieldEditor(PrefsDialog.LOGCAT_FONT, "Text output font:", + getFieldEditorParent()); + addField(ffe); + } + } + + + /** + * "Application" prefs page. + */ + private static class AppPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public AppPrefs() { + super(FLAT); // use "flat" layout + setTitle("DDMS"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + RadioGroupFieldEditor rgfe; + + rgfe = new RadioGroupFieldEditor(PREFS_LOG_LEVEL, + "Logging Level", 1, new String[][] { + { "Verbose", LogLevel.VERBOSE.getStringValue() }, + { "Debug", LogLevel.DEBUG.getStringValue() }, + { "Info", LogLevel.INFO.getStringValue() }, + { "Warning", LogLevel.WARN.getStringValue() }, + { "Error", LogLevel.ERROR.getStringValue() }, + { "Assert", LogLevel.ASSERT.getStringValue() }, + }, + getFieldEditorParent(), true); + addField(rgfe); + } + } + + /** + * "Device" prefs page. + */ + private static class UsageStatsPrefs extends PreferencePage { + + private BooleanFieldEditor mOptInCheckbox; + private Composite mTop; + + /** + * Basic constructor. + */ + public UsageStatsPrefs() { + setTitle("Usage Stats"); + } + + @Override + protected Control createContents(Composite parent) { + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Link text = new Link(mTop, SWT.WRAP); + text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + text.setText(SdkStatsService.BODY_TEXT); + + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + SdkStatsService.openUrl(event.text); + } + }); + + mOptInCheckbox = new BooleanFieldEditor(SdkStatsService.PING_OPT_IN, + SdkStatsService.CHECKBOX_TEXT, mTop); + mOptInCheckbox.setPage(this); + mOptInCheckbox.setPreferenceStore(getPreferenceStore()); + mOptInCheckbox.load(); + + return null; + } + + @Override + protected Point doComputeSize() { + if (mTop != null) { + return mTop.computeSize(450, SWT.DEFAULT, true); + } + + return super.doComputeSize(); + } + + @Override + protected void performDefaults() { + if (mOptInCheckbox != null) { + mOptInCheckbox.loadDefault(); + } + super.performDefaults(); + } + + @Override + public void performApply() { + if (mOptInCheckbox != null) { + mOptInCheckbox.store(); + } + super.performApply(); + } + + @Override + public boolean performOk() { + if (mOptInCheckbox != null) { + mOptInCheckbox.store(); + } + return super.performOk(); + } + } + +} + + diff --git a/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java new file mode 100644 index 0000000..d00bc7f --- /dev/null +++ b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddms; + +import com.android.ddmuilib.TableHelper; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Dialog to configure the static debug ports. + * + */ +public class StaticPortConfigDialog extends Dialog { + + /** Preference name for the 0th column width */ + private static final String PREFS_DEVICE_COL = "spcd.deviceColumn"; //$NON-NLS-1$ + + /** Preference name for the 1st column width */ + private static final String PREFS_APP_COL = "spcd.AppColumn"; //$NON-NLS-1$ + + /** Preference name for the 2nd column width */ + private static final String PREFS_PORT_COL = "spcd.PortColumn"; //$NON-NLS-1$ + + private static final int COL_DEVICE = 0; + private static final int COL_APPLICATION = 1; + private static final int COL_PORT = 2; + + + private static final int DLG_WIDTH = 500; + private static final int DLG_HEIGHT = 300; + + private Shell mShell; + private Shell mParent; + + private Table mPortTable; + + /** + * Array containing the list of already used static port to avoid + * duplication. + */ + private ArrayList<Integer> mPorts = new ArrayList<Integer>(); + + /** + * Basic constructor. + * @param parent + */ + public StaticPortConfigDialog(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Open and display the dialog. This method returns only when the + * user closes the dialog somehow. + * + */ + public void open() { + createUI(); + + if (mParent == null || mShell == null) { + return; + } + + updateFromStore(); + + // Set the dialog size. + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.pack(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + } + + /** + * Creates the dialog ui. + */ + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Static Port Configuration"); + + mShell.setLayout(new GridLayout(1, true)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + + // center part with the list on the left and the buttons + // on the right. + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(2, false)); + + // left part: list view + mPortTable = new Table(main, SWT.SINGLE | SWT.FULL_SELECTION); + mPortTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mPortTable.setHeaderVisible(true); + mPortTable.setLinesVisible(true); + + TableHelper.createTableColumn(mPortTable, "Device Serial Number", + SWT.LEFT, "emulator-5554", //$NON-NLS-1$ + PREFS_DEVICE_COL, PrefsDialog.getStore()); + + TableHelper.createTableColumn(mPortTable, "Application Package", + SWT.LEFT, "com.android.samples.phone", //$NON-NLS-1$ + PREFS_APP_COL, PrefsDialog.getStore()); + + TableHelper.createTableColumn(mPortTable, "Debug Port", + SWT.RIGHT, "Debug Port", //$NON-NLS-1$ + PREFS_PORT_COL, PrefsDialog.getStore()); + + // right part: buttons + Composite buttons = new Composite(main, SWT.NONE); + buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + buttons.setLayout(new GridLayout(1, true)); + + Button newButton = new Button(buttons, SWT.NONE); + newButton.setText("New..."); + newButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, + mPorts); + if (dlg.open()) { + // get the text + String device = dlg.getDeviceSN(); + String app = dlg.getAppName(); + int port = dlg.getPortNumber(); + + // add it to the list + addEntry(device, app, port); + } + } + }); + + final Button editButton = new Button(buttons, SWT.NONE); + editButton.setText("Edit..."); + editButton.setEnabled(false); + editButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mPortTable.getSelectionIndex(); + String oldDeviceName = getDeviceName(index); + String oldAppName = getAppName(index); + String oldPortNumber = getPortNumber(index); + StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, + mPorts, oldDeviceName, oldAppName, oldPortNumber); + if (dlg.open()) { + // get the text + String deviceName = dlg.getDeviceSN(); + String app = dlg.getAppName(); + int port = dlg.getPortNumber(); + + // add it to the list + replaceEntry(index, deviceName, app, port); + } + } + }); + + final Button deleteButton = new Button(buttons, SWT.NONE); + deleteButton.setText("Delete"); + deleteButton.setEnabled(false); + deleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mPortTable.getSelectionIndex(); + removeEntry(index); + } + }); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + Button okButton = new Button(bottomComp, SWT.NONE); + okButton.setText("OK"); + okButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateStore(); + mShell.close(); + } + }); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + mPortTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mPortTable.getSelectionIndex(); + + boolean enabled = index != -1; + editButton.setEnabled(enabled); + deleteButton.setEnabled(enabled); + } + }); + + mShell.pack(); + + } + + /** + * Add a new entry in the list. + * @param deviceName the serial number of the device + * @param appName java package for the application + * @param portNumber port number + */ + private void addEntry(String deviceName, String appName, int portNumber) { + // create a new item for the table + TableItem item = new TableItem(mPortTable, SWT.NONE); + + item.setText(COL_DEVICE, deviceName); + item.setText(COL_APPLICATION, appName); + item.setText(COL_PORT, Integer.toString(portNumber)); + + // add the port to the list of port number used. + mPorts.add(portNumber); + } + + /** + * Remove an entry from the list. + * @param index The index of the entry to be removed + */ + private void removeEntry(int index) { + // remove from the ui + mPortTable.remove(index); + + // and from the port list. + mPorts.remove(index); + } + + /** + * Replace an entry in the list with new values. + * @param index The index of the item to be replaced + * @param deviceName the serial number of the device + * @param appName The new java package for the application + * @param portNumber The new port number. + */ + private void replaceEntry(int index, String deviceName, String appName, int portNumber) { + // get the table item by index + TableItem item = mPortTable.getItem(index); + + // set its new value + item.setText(COL_DEVICE, deviceName); + item.setText(COL_APPLICATION, appName); + item.setText(COL_PORT, Integer.toString(portNumber)); + + // and replace the port number in the port list. + mPorts.set(index, portNumber); + } + + + /** + * Returns the device name for a specific index + * @param index The index + * @return the java package name of the application + */ + private String getDeviceName(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_DEVICE); + } + + /** + * Returns the application name for a specific index + * @param index The index + * @return the java package name of the application + */ + private String getAppName(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_APPLICATION); + } + + /** + * Returns the port number for a specific index + * @param index The index + * @return the port number + */ + private String getPortNumber(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_PORT); + } + + /** + * Updates the ui from the value in the preference store. + */ + private void updateFromStore() { + // get the map from the debug port manager + DebugPortProvider provider = DebugPortProvider.getInstance(); + Map<String, Map<String, Integer>> map = provider.getPortList(); + + // we're going to loop on the keys and fill the table. + Set<String> deviceKeys = map.keySet(); + + for (String deviceKey : deviceKeys) { + Map<String, Integer> deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set<String> appKeys = deviceMap.keySet(); + + for (String appKey : appKeys) { + Integer port = deviceMap.get(appKey); + if (port != null) { + addEntry(deviceKey, appKey, port); + } + } + } + } + } + + /** + * Update the store from the content of the ui. + */ + private void updateStore() { + // create a new Map object and fill it. + HashMap<String, Map<String, Integer>> map = new HashMap<String, Map<String, Integer>>(); + + int count = mPortTable.getItemCount(); + + for (int i = 0 ; i < count ; i++) { + TableItem item = mPortTable.getItem(i); + String deviceName = item.getText(COL_DEVICE); + + Map<String, Integer> deviceMap = map.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap<String, Integer>(); + map.put(deviceName, deviceMap); + } + + deviceMap.put(item.getText(COL_APPLICATION), Integer.valueOf(item.getText(COL_PORT))); + } + + // set it in the store through the debug port manager. + DebugPortProvider provider = DebugPortProvider.getInstance(); + provider.setPortList(map); + } +} diff --git a/ddms/app/src/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java new file mode 100644 index 0000000..6330126 --- /dev/null +++ b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddms; + +import com.android.ddmlib.Device; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; + +/** + * Small dialog box to edit a static port number. + */ +public class StaticPortEditDialog extends Dialog { + + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 200; + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + private String mAppName; + + private String mPortNumber; + + private Button mOkButton; + + private Label mWarning; + + /** List of ports already in use */ + private ArrayList<Integer> mPorts; + + /** This is the port being edited. */ + private int mEditPort = -1; + private String mDeviceSn; + + /** + * Creates a dialog with empty fields. + * @param parent The parent Shell + * @param ports The list of already used port numbers. + */ + public StaticPortEditDialog(Shell parent, ArrayList<Integer> ports) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mPorts = ports; + mDeviceSn = Device.FIRST_EMULATOR_SN; + } + + /** + * Creates a dialog with predefined values. + * @param shell The parent shell + * @param ports The list of already used port numbers. + * @param oldDeviceSN the device serial number to display + * @param oldAppName The application name to display + * @param oldPortNumber The port number to display + */ + public StaticPortEditDialog(Shell shell, ArrayList<Integer> ports, + String oldDeviceSN, String oldAppName, String oldPortNumber) { + this(shell, ports); + + mDeviceSn = oldDeviceSN; + mAppName = oldAppName; + mPortNumber = oldPortNumber; + mEditPort = Integer.valueOf(mPortNumber); + } + + /** + * Opens the dialog. The method will return when the user closes the dialog + * somehow. + * + * @return true if ok was pressed, false if cancelled. + */ + public boolean open() { + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.open(); + + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mOk; + } + + public String getDeviceSN() { + return mDeviceSn; + } + + public String getAppName() { + return mAppName; + } + + public int getPortNumber() { + return Integer.valueOf(mPortNumber); + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Static Port"); + + mShell.setLayout(new GridLayout(1, false)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + } + }); + + // center part with the edit field + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(2, false)); + + Label l0 = new Label(main, SWT.NONE); + l0.setText("Device Name:"); + + final Text deviceSNText = new Text(main, SWT.SINGLE | SWT.BORDER); + deviceSNText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (mDeviceSn != null) { + deviceSNText.setText(mDeviceSn); + } + deviceSNText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mDeviceSn = deviceSNText.getText().trim(); + validate(); + } + }); + + Label l = new Label(main, SWT.NONE); + l.setText("Application Name:"); + + final Text appNameText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mAppName != null) { + appNameText.setText(mAppName); + } + appNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + appNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mAppName = appNameText.getText().trim(); + validate(); + } + }); + + Label l2 = new Label(main, SWT.NONE); + l2.setText("Debug Port:"); + + final Text debugPortText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mPortNumber != null) { + debugPortText.setText(mPortNumber); + } + debugPortText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + debugPortText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mPortNumber = debugPortText.getText().trim(); + validate(); + } + }); + + // warning label + Composite warningComp = new Composite(mShell, SWT.NONE); + warningComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + warningComp.setLayout(new GridLayout(1, true)); + + mWarning = new Label(warningComp, SWT.NONE); + mWarning.setText(""); + mWarning.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + // first we reset the warning dialog. This allows us to latter + // display warnings. + mWarning.setText(""); // $NON-NLS-1$ + + // check the device name field is not empty + if (mDeviceSn == null || mDeviceSn.length() == 0) { + mWarning.setText("Device name missing."); + mOkButton.setEnabled(false); + return; + } + + // check the application name field is not empty + if (mAppName == null || mAppName.length() == 0) { + mWarning.setText("Application name missing."); + mOkButton.setEnabled(false); + return; + } + + String packageError = "Application name must be a valid Java package name."; + + // validate the package name as well. It must be a fully qualified + // java package. + String[] packageSegments = mAppName.split("\\."); // $NON-NLS-1$ + for (String p : packageSegments) { + if (p.matches("^[a-zA-Z][a-zA-Z0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText(packageError); + mOkButton.setEnabled(false); + return; + } + + // lets also display a warning if the package contains upper case + // letters. + if (p.matches("^[a-z][a-z0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText("Lower case is recommended for Java packages."); + } + } + + // the split will not detect the last char being a '.' + // so we test it manually + if (mAppName.charAt(mAppName.length()-1) == '.') { + mWarning.setText(packageError); + mOkButton.setEnabled(false); + return; + } + + // now we test the package name field is not empty. + if (mPortNumber == null || mPortNumber.length() == 0) { + mWarning.setText("Port Number missing."); + mOkButton.setEnabled(false); + return; + } + + // then we check it only contains digits. + if (mPortNumber.matches("[0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText("Port Number invalid."); + mOkButton.setEnabled(false); + return; + } + + // get the int from the port number to validate + long port = Long.valueOf(mPortNumber); + if (port >= 32767) { + mOkButton.setEnabled(false); + return; + } + + // check if its in the list of already used ports + if (port != mEditPort) { + for (Integer i : mPorts) { + if (port == i.intValue()) { + mWarning.setText("Port already in use."); + mOkButton.setEnabled(false); + return; + } + } + } + + // at this point there's not error, so we enable the ok button. + mOkButton.setEnabled(true); + } +} diff --git a/ddms/app/src/com/android/ddms/UIThread.java b/ddms/app/src/com/android/ddms/UIThread.java new file mode 100644 index 0000000..ff89e2c --- /dev/null +++ b/ddms/app/src/com/android/ddms/UIThread.java @@ -0,0 +1,1491 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddms; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.ILogOutput; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.AllocationPanel; +import com.android.ddmuilib.DevicePanel; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.EmulatorControlPanel; +import com.android.ddmuilib.HeapPanel; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.ImageHelper; +import com.android.ddmuilib.ImageLoader; +import com.android.ddmuilib.InfoPanel; +import com.android.ddmuilib.NativeHeapPanel; +import com.android.ddmuilib.ScreenShotDialog; +import com.android.ddmuilib.SysinfoPanel; +import com.android.ddmuilib.TablePanel; +import com.android.ddmuilib.ThreadPanel; +import com.android.ddmuilib.actions.ToolItemAction; +import com.android.ddmuilib.explorer.DeviceExplorer; +import com.android.ddmuilib.log.event.EventLogPanel; +import com.android.ddmuilib.logcat.LogColors; +import com.android.ddmuilib.logcat.LogFilter; +import com.android.ddmuilib.logcat.LogPanel; +import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTError; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.MenuAdapter; +import org.eclipse.swt.events.MenuEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.events.ShellListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +import java.io.File; +import java.util.ArrayList; + +/** + * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an + * SWT application. So this class mainly builds the ui, and manages communication between the panels + * when {@link Device} / {@link Client} selection changes. + */ +public class UIThread implements IUiSelectionListener { + /* + * UI tab panel definitions. The constants here must match up with the array + * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing + * the client list. + */ + public static final int PANEL_CLIENT_LIST = -1; + + public static final int PANEL_INFO = 0; + + public static final int PANEL_THREAD = 1; + + public static final int PANEL_HEAP = 2; + + public static final int PANEL_NATIVE_HEAP = 3; + + private static final int PANEL_ALLOCATIONS = 4; + + private static final int PANEL_SYSINFO = 5; + + private static final int PANEL_COUNT = 6; + + /** Content is setup in the constructor */ + private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT]; + + private static final String[] mPanelNames = new String[] { + "Info", "Threads", "VM Heap", "Native Heap", "Allocation Tracker", "Sysinfo" + }; + + private static final String[] mPanelTips = new String[] { + "Client information", "Thread status", "VM heap status", + "Native heap status", "Allocation Tracker", "Sysinfo graphs" + }; + + private static final String PREFERENCE_LOGSASH = + "logSashLocation"; //$NON-NLS-1$ + private static final String PREFERENCE_SASH = + "sashLocation"; //$NON-NLS-1$ + + private static final String PREFS_COL_TIME = + "logcat.time"; //$NON-NLS-1$ + private static final String PREFS_COL_LEVEL = + "logcat.level"; //$NON-NLS-1$ + private static final String PREFS_COL_PID = + "logcat.pid"; //$NON-NLS-1$ + private static final String PREFS_COL_TAG = + "logcat.tag"; //$NON-NLS-1$ + private static final String PREFS_COL_MESSAGE = + "logcat.message"; //$NON-NLS-1$ + + private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$ + + // singleton instance + private static UIThread mInstance = new UIThread(); + + // our display + private Display mDisplay; + + // the table we show in the left-hand pane + private DevicePanel mDevicePanel; + + private Device mCurrentDevice = null; + private Client mCurrentClient = null; + + // status line at the bottom of the app window + private Label mStatusLine; + + // some toolbar items we need to update + private ToolItem mTBShowThreadUpdates; + private ToolItem mTBShowHeapUpdates; + private ToolItem mTBHalt; + private ToolItem mTBCauseGc; + + private ImageLoader mDdmsImageLoader; + private ImageLoader mDdmuiLibImageLoader; + + private final class FilterStorage implements ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore() { + String filterPrefs = PrefsDialog.getStore().getString( + PREFS_FILTERS); + + // split in a string per filter + String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$ + + ArrayList<LogFilter> list = + new ArrayList<LogFilter>(filters.length); + + for (String f : filters) { + if (f.length() > 0) { + LogFilter logFilter = new LogFilter(); + if (logFilter.loadFromString(f)) { + list.add(logFilter); + } + } + } + + return list.toArray(new LogFilter[list.size()]); + } + + public void saveFilters(LogFilter[] filters) { + StringBuilder sb = new StringBuilder(); + for (LogFilter f : filters) { + String filterString = f.toString(); + sb.append(filterString); + sb.append('|'); + } + + PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString()); + } + + public boolean requiresDefaultFilter() { + return true; + } + } + + + private LogPanel mLogPanel; + + private ToolItemAction mCreateFilterAction; + private ToolItemAction mDeleteFilterAction; + private ToolItemAction mEditFilterAction; + private ToolItemAction mExportAction; + private ToolItemAction mClearAction; + + private ToolItemAction[] mLogLevelActions; + private String[] mLogLevelIcons = { + "v.png", //$NON-NLS-1S + "d.png", //$NON-NLS-1S + "i.png", //$NON-NLS-1S + "w.png", //$NON-NLS-1S + "e.png", //$NON-NLS-1S + }; + + protected Clipboard mClipboard; + + private MenuItem mCopyMenuItem; + + private MenuItem mSelectAllMenuItem; + + private TableFocusListener mTableListener; + + private DeviceExplorer mExplorer = null; + private Shell mExplorerShell = null; + + private EmulatorControlPanel mEmulatorPanel; + + private EventLogPanel mEventLogPanel; + + private class TableFocusListener implements ITableFocusListener { + + private IFocusedTableActivator mCurrentActivator; + + public void focusGained(IFocusedTableActivator activator) { + mCurrentActivator = activator; + if (mCopyMenuItem.isDisposed() == false) { + mCopyMenuItem.setEnabled(true); + mSelectAllMenuItem.setEnabled(true); + } + } + + public void focusLost(IFocusedTableActivator activator) { + // if we move from one table to another, it's unclear + // if the old table lose its focus before the new + // one gets the focus, so we need to check. + if (activator == mCurrentActivator) { + activator = null; + if (mCopyMenuItem.isDisposed() == false) { + mCopyMenuItem.setEnabled(false); + mSelectAllMenuItem.setEnabled(false); + } + } + } + + public void copy(Clipboard clipboard) { + if (mCurrentActivator != null) { + mCurrentActivator.copy(clipboard); + } + } + + public void selectAll() { + if (mCurrentActivator != null) { + mCurrentActivator.selectAll(); + } + } + + } + + + /** + * Generic constructor. + */ + private UIThread() { + mPanels[PANEL_INFO] = new InfoPanel(); + mPanels[PANEL_THREAD] = new ThreadPanel(); + mPanels[PANEL_HEAP] = new HeapPanel(); + if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) { + mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel(); + } else { + mPanels[PANEL_NATIVE_HEAP] = null; + } + mPanels[PANEL_ALLOCATIONS] = new AllocationPanel(); + mPanels[PANEL_SYSINFO] = new SysinfoPanel(); + } + + /** + * Get singleton instance of the UI thread. + */ + public static UIThread getInstance() { + return mInstance; + } + + /** + * Return the Display. Don't try this unless you're in the UI thread. + */ + public Display getDisplay() { + return mDisplay; + } + + public void asyncExec(Runnable r) { + if (mDisplay != null && mDisplay.isDisposed() == false) { + mDisplay.asyncExec(r); + } + } + + /** returns the IPreferenceStore */ + public IPreferenceStore getStore() { + return PrefsDialog.getStore(); + } + + /** + * Create SWT objects and drive the user interface event loop. + */ + public void runUI() { + Display.setAppName("ddms"); + mDisplay = new Display(); + Shell shell = new Shell(mDisplay); + + // create the image loaders for DDMS and DDMUILIB + mDdmsImageLoader = new ImageLoader(this.getClass()); + mDdmuiLibImageLoader = new ImageLoader(DevicePanel.class); + + shell.setImage(ImageHelper.loadImage(mDdmsImageLoader, mDisplay, + "ddms-icon.png", //$NON-NLS-1$ + 100, 50, null)); + + Log.setLogOutput(new ILogOutput() { + public void printAndPromptLog(final LogLevel logLevel, final String tag, + final String message) { + Log.printLog(logLevel, tag, message); + // dialog box only run in UI thread.. + mDisplay.asyncExec(new Runnable() { + public void run() { + Shell shell = mDisplay.getActiveShell(); + if (logLevel == LogLevel.ERROR) { + MessageDialog.openError(shell, tag, message); + } else { + MessageDialog.openWarning(shell, tag, message); + } + } + }); + } + + public void printLog(LogLevel logLevel, String tag, String message) { + Log.printLog(logLevel, tag, message); + } + }); + + // [try to] ensure ADB is running + String adbLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ + if (adbLocation != null && adbLocation.length() != 0) { + adbLocation += File.separator + "adb"; //$NON-NLS-1$ + } else { + adbLocation = "adb"; //$NON-NLS-1$ + } + + AndroidDebugBridge.init(true /* debugger support */); + AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */); + + shell.setText("Dalvik Debug Monitor"); + setConfirmClose(shell); + createMenus(shell); + createWidgets(shell); + + shell.pack(); + setSizeAndPosition(shell); + shell.open(); + + Log.d("ddms", "UI is up"); + + while (!shell.isDisposed()) { + if (!mDisplay.readAndDispatch()) + mDisplay.sleep(); + } + mLogPanel.stopLogCat(true); + + mDevicePanel.dispose(); + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.dispose(); + } + } + + mDisplay.dispose(); + Log.d("ddms", "UI is down"); + } + + /** + * Set the size and position of the main window from the preference, and + * setup listeners for control events (resize/move of the window) + */ + private void setSizeAndPosition(final Shell shell) { + shell.setMinimumSize(400, 200); + + // get the x/y and w/h from the prefs + PreferenceStore prefs = PrefsDialog.getStore(); + int x = prefs.getInt(PrefsDialog.SHELL_X); + int y = prefs.getInt(PrefsDialog.SHELL_Y); + int w = prefs.getInt(PrefsDialog.SHELL_WIDTH); + int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT); + + // check that we're not out of the display area + Rectangle rect = mDisplay.getClientArea(); + // first check the width/height + if (w > rect.width) { + w = rect.width; + prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width); + } + if (h > rect.height) { + h = rect.height; + prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height); + } + // then check x. Make sure the left corner is in the screen + if (x < rect.x) { + x = rect.x; + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + } else if (x >= rect.x + rect.width) { + x = rect.x + rect.width - w; + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + } + // then check y. Make sure the left corner is in the screen + if (y < rect.y) { + y = rect.y; + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } else if (y >= rect.y + rect.height) { + y = rect.y + rect.height - h; + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } + + // now we can set the location/size + shell.setBounds(x, y, w, h); + + // add listener for resize/move + shell.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + // get the new x/y + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } + + public void controlResized(ControlEvent e) { + // get the new w/h + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width); + prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height); + } + }); + } + + /** + * Set the size and position of the file explorer window from the + * preference, and setup listeners for control events (resize/move of + * the window) + */ + private void setExplorerSizeAndPosition(final Shell shell) { + shell.setMinimumSize(400, 200); + + // get the x/y and w/h from the prefs + PreferenceStore prefs = PrefsDialog.getStore(); + int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X); + int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y); + int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH); + int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT); + + // check that we're not out of the display area + Rectangle rect = mDisplay.getClientArea(); + // first check the width/height + if (w > rect.width) { + w = rect.width; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width); + } + if (h > rect.height) { + h = rect.height; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height); + } + // then check x. Make sure the left corner is in the screen + if (x < rect.x) { + x = rect.x; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + } else if (x >= rect.x + rect.width) { + x = rect.x + rect.width - w; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + } + // then check y. Make sure the left corner is in the screen + if (y < rect.y) { + y = rect.y; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } else if (y >= rect.y + rect.height) { + y = rect.y + rect.height - h; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } + + // now we can set the location/size + shell.setBounds(x, y, w, h); + + // add listener for resize/move + shell.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + // get the new x/y + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } + + public void controlResized(ControlEvent e) { + // get the new w/h + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height); + } + }); + } + + /* + * Set the confirm-before-close dialog. TODO: enable/disable in prefs. TODO: + * is there any point in having this? + */ + private void setConfirmClose(final Shell shell) { + if (true) + return; + + shell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + int style = SWT.APPLICATION_MODAL | SWT.YES | SWT.NO; + MessageBox msgBox = new MessageBox(shell, style); + msgBox.setText("Confirm..."); + msgBox.setMessage("Close DDM?"); + event.doit = (msgBox.open() == SWT.YES); + } + }); + } + + /* + * Create the menu bar and items. + */ + private void createMenus(final Shell shell) { + // create menu bar + Menu menuBar = new Menu(shell, SWT.BAR); + + // create top-level items + MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE); + fileItem.setText("&File"); + MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE); + editItem.setText("&Edit"); + MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE); + actionItem.setText("&Actions"); + MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE); + deviceItem.setText("&Device"); + MenuItem helpItem = new MenuItem(menuBar, SWT.CASCADE); + helpItem.setText("&Help"); + + // create top-level menus + Menu fileMenu = new Menu(menuBar); + fileItem.setMenu(fileMenu); + Menu editMenu = new Menu(menuBar); + editItem.setMenu(editMenu); + Menu actionMenu = new Menu(menuBar); + actionItem.setMenu(actionMenu); + Menu deviceMenu = new Menu(menuBar); + deviceItem.setMenu(deviceMenu); + Menu helpMenu = new Menu(menuBar); + helpItem.setMenu(helpMenu); + + MenuItem item; + + // create File menu items + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("&Preferences..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + PrefsDialog.run(shell); + } + }); + + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("&Static Port Configuration..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell); + dlg.open(); + } + }); + + new MenuItem(fileMenu, SWT.SEPARATOR); + + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("E&xit\tCtrl-Q"); + item.setAccelerator('Q' | SWT.CONTROL); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + // create edit menu items + mCopyMenuItem = new MenuItem(editMenu, SWT.NONE); + mCopyMenuItem.setText("&Copy\tCtrl-C"); + mCopyMenuItem.setAccelerator('C' | SWT.COMMAND); + mCopyMenuItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mTableListener.copy(mClipboard); + } + }); + + new MenuItem(editMenu, SWT.SEPARATOR); + + mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE); + mSelectAllMenuItem.setText("Select &All\tCtrl-A"); + mSelectAllMenuItem.setAccelerator('A' | SWT.COMMAND); + mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mTableListener.selectAll(); + } + }); + + // create Action menu items + // TODO: this should come with a confirmation dialog + final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE); + actionHaltItem.setText("&Halt VM"); + actionHaltItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.killSelectedClient(); + } + }); + + final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE); + actionCauseGcItem.setText("Cause &GC"); + actionCauseGcItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.forceGcOnSelectedClient(); + } + }); + + // configure Action items based on current state + actionMenu.addMenuListener(new MenuAdapter() { + @Override + public void menuShown(MenuEvent e) { + actionHaltItem.setEnabled(mTBHalt.getEnabled()); + actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled()); + } + }); + + // create Device menu items + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("&Screen capture...\tCTrl-S"); + item.setAccelerator('S' | SWT.CONTROL); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentDevice != null) { + ScreenShotDialog dlg = new ScreenShotDialog(shell); + dlg.open(mCurrentDevice); + } + } + }); + + new MenuItem(deviceMenu, SWT.SEPARATOR); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("File Explorer..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createFileExplorer(); + } + }); + + new MenuItem(deviceMenu, SWT.SEPARATOR); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("Show &process status..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("Dump &device state..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0", + "device-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("Dump &app state..."); + item.setEnabled(false); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("Dump &radio state..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog( + "cat /data/logs/radio.4 /data/logs/radio.3" + + " /data/logs/radio.2 /data/logs/radio.1" + + " /data/logs/radio", + "radio-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + item = new MenuItem(deviceMenu, SWT.NONE); + item.setText("Run &logcat..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt", + shell); + dlg.open(mCurrentDevice); + } + }); + + // create Help menu items + item = new MenuItem(helpMenu, SWT.NONE); + item.setText("&Contents..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int style = SWT.APPLICATION_MODAL | SWT.OK; + MessageBox msgBox = new MessageBox(shell, style); + msgBox.setText("Help!"); + msgBox.setMessage("Help wanted."); + msgBox.open(); + } + }); + + new MenuItem(helpMenu, SWT.SEPARATOR); + + item = new MenuItem(helpMenu, SWT.NONE); + item.setText("&About..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + AboutDialog dlg = new AboutDialog(shell); + dlg.open(); + } + }); + + // tell the shell to use this menu + shell.setMenuBar(menuBar); + } + + /* + * Create the widgets in the main application window. The basic layout is a + * two-panel sash, with a scrolling list of VMs on the left and detailed + * output for a single VM on the right. + */ + private void createWidgets(final Shell shell) { + Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + + /* + * Create three areas: tool bar, split panels, status line + */ + shell.setLayout(new GridLayout(1, false)); + + // 1. panel area + final Composite panelArea = new Composite(shell, SWT.BORDER); + + // make the panel area absorb all space + panelArea.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // 2. status line. + mStatusLine = new Label(shell, SWT.NONE); + + // make status line extend all the way across + mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStatusLine.setText("Initializing..."); + + /* + * Configure the split-panel area. + */ + final PreferenceStore prefs = PrefsDialog.getStore(); + + Composite topPanel = new Composite(panelArea, SWT.NONE); + final Sash sash = new Sash(panelArea, SWT.HORIZONTAL); + sash.setBackground(darkGray); + Composite bottomPanel = new Composite(panelArea, SWT.NONE); + + panelArea.setLayout(new FormLayout()); + + createTopPanel(topPanel, darkGray); + createBottomPanel(bottomPanel); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topPanel.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) { + sashData.top = new FormAttachment(0, prefs.getInt( + PREFERENCE_LOGSASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + bottomPanel.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = panelArea.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFERENCE_LOGSASH, e.y); + panelArea.layout(); + } + } + }); + + // add a global focus listener for all the tables + mTableListener = new TableFocusListener(); + + // now set up the listener in the various panels + mLogPanel.setTableFocusListener(mTableListener); + mEventLogPanel.setTableFocusListener(mTableListener); + for (TablePanel p : mPanels) { + if (p != null) { + p.setTableFocusListener(mTableListener); + } + } + + mStatusLine.setText(""); + } + + /* + * Populate the tool bar. + */ + private void createDevicePanelToolBar(ToolBar toolBar) { + Display display = toolBar.getDisplay(); + + // add "show thread updates" button + mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK); + mTBShowThreadUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBShowThreadUpdates.setToolTipText("Show thread updates"); + mTBShowThreadUpdates.setEnabled(false); + mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentClient != null) { + // boolean status = ((ToolItem)e.item).getSelection(); + // invert previous state + boolean enable = !mCurrentClient.isThreadUpdateEnabled(); + + mCurrentClient.setThreadUpdateEnabled(enable); + } else { + e.doit = false; // this has no effect? + } + } + }); + + // add "show heap updates" button + mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK); + mTBShowHeapUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBShowHeapUpdates.setToolTipText("Show heap updates"); + mTBShowHeapUpdates.setEnabled(false); + mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentClient != null) { + // boolean status = ((ToolItem)e.item).getSelection(); + // invert previous state + boolean enable = !mCurrentClient.isHeapUpdateEnabled(); + mCurrentClient.setHeapUpdateEnabled(enable); + } else { + e.doit = false; // this has no effect? + } + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + // add "kill VM" button; need to make this visually distinct from + // the status update buttons + mTBHalt = new ToolItem(toolBar, SWT.PUSH); + mTBHalt.setToolTipText("Halt the target VM"); + mTBHalt.setEnabled(false); + mTBHalt.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBHalt.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.killSelectedClient(); + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + // add "cause GC" button + mTBCauseGc = new ToolItem(toolBar, SWT.PUSH); + mTBCauseGc.setToolTipText("Cause an immediate GC in the target VM"); + mTBCauseGc.setEnabled(false); + mTBCauseGc.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBCauseGc.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.forceGcOnSelectedClient(); + } + }); + + toolBar.pack(); + } + + private void createTopPanel(final Composite comp, Color darkGray) { + final PreferenceStore prefs = PrefsDialog.getStore(); + + comp.setLayout(new FormLayout()); + + Composite leftPanel = new Composite(comp, SWT.NONE); + final Sash sash = new Sash(comp, SWT.VERTICAL); + sash.setBackground(darkGray); + Composite rightPanel = new Composite(comp, SWT.NONE); + + createLeftPanel(leftPanel); + createRightPanel(rightPanel); + + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + leftPanel.setLayoutData(data); + + final FormData sashData = new FormData(); + sashData.top = new FormAttachment(0, 0); + sashData.bottom = new FormAttachment(100, 0); + if (prefs != null && prefs.contains(PREFERENCE_SASH)) { + sashData.left = new FormAttachment(0, prefs.getInt( + PREFERENCE_SASH)); + } else { + // position the sash 380 from the right instead of x% (done by using + // FormAttachment(x, 0)) in order to keep the sash at the same + // position + // from the left when the window is resized. + // 380px is just enough to display the left table with no horizontal + // scrollbar with the default font. + sashData.left = new FormAttachment(0, 380); + } + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + rightPanel.setLayoutData(data); + + final int minPanelWidth = 60; + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = comp.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFERENCE_SASH, e.x); + comp.layout(); + } + } + }); + } + + private void createBottomPanel(final Composite comp) { + final PreferenceStore prefs = PrefsDialog.getStore(); + + // create clipboard + Display display = comp.getDisplay(); + mClipboard = new Clipboard(display); + + LogColors colors = new LogColors(); + + colors.infoColor = new Color(display, 0, 127, 0); + colors.debugColor = new Color(display, 0, 0, 127); + colors.errorColor = new Color(display, 255, 0, 0); + colors.warningColor = new Color(display, 255, 127, 0); + colors.verboseColor = new Color(display, 0, 0, 0); + + // set the preferences names + LogPanel.PREFS_TIME = PREFS_COL_TIME; + LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL; + LogPanel.PREFS_PID = PREFS_COL_PID; + LogPanel.PREFS_TAG = PREFS_COL_TAG; + LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE; + + comp.setLayout(new GridLayout(1, false)); + + ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL); + + mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mCreateFilterAction.item.setToolTipText("Create Filter"); + mCreateFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "add.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.addFilter(); + } + }); + + mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mEditFilterAction.item.setToolTipText("Edit Filter"); + mEditFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "edit.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mEditFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.editFilter(); + } + }); + + mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mDeleteFilterAction.item.setToolTipText("Delete Filter"); + mDeleteFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "delete.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.deleteFilter(); + } + }); + + + new ToolItem(toolBar, SWT.SEPARATOR); + + LogLevel[] levels = LogLevel.values(); + mLogLevelActions = new ToolItemAction[mLogLevelIcons.length]; + for (int i = 0 ; i < mLogLevelActions.length; i++) { + String name = levels[i].getStringValue(); + final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK); + mLogLevelActions[i] = newAction; + //newAction.item.setText(name); + newAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // disable the other actions and record current index + for (int i = 0 ; i < mLogLevelActions.length; i++) { + ToolItemAction a = mLogLevelActions[i]; + if (a == newAction) { + a.setChecked(true); + + // set the log level + mLogPanel.setCurrentFilterLogLevel(i+2); + } else { + a.setChecked(false); + } + } + } + }); + + newAction.item.setToolTipText(name); + newAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + mLogLevelIcons[i], + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + } + + new ToolItem(toolBar, SWT.SEPARATOR); + + mClearAction = new ToolItemAction(toolBar, SWT.PUSH); + mClearAction.item.setToolTipText("Clear Log"); + + mClearAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "clear.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mClearAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.clear(); + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + mExportAction = new ToolItemAction(toolBar, SWT.PUSH); + mExportAction.item.setToolTipText("Export Selection As Text..."); + mExportAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "save.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mExportAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.save(); + } + }); + + + toolBar.pack(); + + // now create the log view + mLogPanel = new LogPanel(new ImageLoader(LogPanel.class), colors, new FilterStorage(), + LogPanel.FILTER_MANUAL); + + mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); + + String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE); + if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) { + mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO); + } + + String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT); + if (fontStr != null) { + try { + FontData fdat = new FontData(fontStr); + mLogPanel.setFont(new Font(display, fdat)); + } catch (IllegalArgumentException e) { + // Looks like fontStr isn't a valid font representation. + // We do nothing in this case, the logcat view will use the default font. + } catch (SWTError e2) { + // Looks like the Font() constructor failed. + // We do nothing in this case, the logcat view will use the default font. + } + } + + mLogPanel.createPanel(comp); + + // and start the logcat + mLogPanel.startLogCat(mCurrentDevice); + } + + /* + * Create the contents of the left panel: a table of VMs. + */ + private void createLeftPanel(final Composite comp) { + comp.setLayout(new GridLayout(1, false)); + ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP); + toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + createDevicePanelToolBar(toolBar); + + Composite c = new Composite(comp, SWT.NONE); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mDevicePanel = new DevicePanel(new ImageLoader(DevicePanel.class), true /* showPorts */); + mDevicePanel.createPanel(c); + + // add ourselves to the device panel selection listener + mDevicePanel.addSelectionListener(this); + } + + /* + * Create the contents of the right panel: tabs with VM information. + */ + private void createRightPanel(final Composite comp) { + TabItem item; + TabFolder tabFolder; + + comp.setLayout(new FillLayout()); + + tabFolder = new TabFolder(comp, SWT.NONE); + + for (int i = 0; i < mPanels.length; i++) { + if (mPanels[i] != null) { + item = new TabItem(tabFolder, SWT.NONE); + item.setText(mPanelNames[i]); + item.setToolTipText(mPanelTips[i]); + item.setControl(mPanels[i].createPanel(tabFolder)); + } + } + + // add the emulator control panel to the folders. + item = new TabItem(tabFolder, SWT.NONE); + item.setText("Emulator Control"); + item.setToolTipText("Emulator Control Panel"); + mEmulatorPanel = new EmulatorControlPanel(mDdmuiLibImageLoader); + item.setControl(mEmulatorPanel.createPanel(tabFolder)); + + // add the event log panel to the folders. + item = new TabItem(tabFolder, SWT.NONE); + item.setText("Event Log"); + item.setToolTipText("Event Log"); + + // create the composite that will hold the toolbar and the event log panel. + Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE); + item.setControl(eventLogTopComposite); + eventLogTopComposite.setLayout(new GridLayout(1, false)); + + // create the toolbar and the actions + ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL); + toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH); + optionsAction.item.setToolTipText("Opens the options panel"); + optionsAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "edit.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH); + clearAction.item.setToolTipText("Clears the event log"); + clearAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "clear.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + new ToolItem(toolbar, SWT.SEPARATOR); + + ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH); + saveAction.item.setToolTipText("Saves the event log"); + saveAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "save.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH); + loadAction.item.setToolTipText("Loads an event log"); + loadAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "load.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH); + importBugAction.item.setToolTipText("Imports a bug report"); + importBugAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "importBug.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + // create the event log panel + mEventLogPanel = new EventLogPanel(mDdmuiLibImageLoader); + + // set the external actions + mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction, + importBugAction); + + // create the panel + mEventLogPanel.createPanel(eventLogTopComposite); + } + + private void createFileExplorer() { + if (mExplorer == null) { + mExplorerShell = new Shell(mDisplay); + + // create the ui + mExplorerShell.setLayout(new GridLayout(1, false)); + + // toolbar + action + ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL); + toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH); + pullAction.item.setToolTipText("Pull File from Device"); + pullAction.item.setImage(mDdmuiLibImageLoader.loadImage("pull.png", mDisplay)); //$NON-NLS-1$ + + ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH); + pushAction.item.setToolTipText("Push file onto Device"); + pushAction.item.setImage(mDdmuiLibImageLoader.loadImage("push.png", mDisplay)); //$NON-NLS-1$ + + ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH); + deleteAction.item.setToolTipText("Delete"); + deleteAction.item.setImage(mDdmuiLibImageLoader.loadImage("delete.png", mDisplay)); //$NON-NLS-1$ + + // device explorer + mExplorer = new DeviceExplorer(); + + mExplorer.setImages(mDdmuiLibImageLoader.loadImage("file.png", mDisplay), //$NON-NLS-1$ + mDdmuiLibImageLoader.loadImage("folder.png", mDisplay), //$NON-NLS-1$ + mDdmuiLibImageLoader.loadImage("android.png", mDisplay), //$NON-NLS-1$ + null); + mExplorer.setActions(pushAction, pullAction, deleteAction); + + pullAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.pullSelection(); + } + }); + pullAction.setEnabled(false); + + pushAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.pushIntoSelection(); + } + }); + pushAction.setEnabled(false); + + deleteAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.deleteSelection(); + } + }); + deleteAction.setEnabled(false); + + Composite parent = new Composite(mExplorerShell, SWT.NONE); + parent.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mExplorer.createPanel(parent); + mExplorer.switchDevice(mCurrentDevice); + + mExplorerShell.addShellListener(new ShellListener() { + public void shellActivated(ShellEvent e) { + // pass + } + + public void shellClosed(ShellEvent e) { + mExplorer = null; + mExplorerShell = null; + } + + public void shellDeactivated(ShellEvent e) { + // pass + } + + public void shellDeiconified(ShellEvent e) { + // pass + } + + public void shellIconified(ShellEvent e) { + // pass + } + }); + + mExplorerShell.pack(); + setExplorerSizeAndPosition(mExplorerShell); + mExplorerShell.open(); + } else { + if (mExplorerShell != null) { + mExplorerShell.forceActive(); + } + } + } + + /** + * Set the status line. TODO: make this a stack, so we can safely have + * multiple things trying to set it all at once. Also specify an expiration? + */ + public void setStatusLine(final String str) { + try { + mDisplay.asyncExec(new Runnable() { + public void run() { + doSetStatusLine(str); + } + }); + } catch (SWTException swte) { + if (!mDisplay.isDisposed()) + throw swte; + } + } + + private void doSetStatusLine(String str) { + if (mStatusLine.isDisposed()) + return; + + if (!mStatusLine.getText().equals(str)) { + mStatusLine.setText(str); + + // try { Thread.sleep(100); } + // catch (InterruptedException ie) {} + } + } + + public void displayError(final String msg) { + try { + mDisplay.syncExec(new Runnable() { + public void run() { + MessageDialog.openError(mDisplay.getActiveShell(), "Error", + msg); + } + }); + } catch (SWTException swte) { + if (!mDisplay.isDisposed()) + throw swte; + } + } + + private void enableButtons() { + if (mCurrentClient != null) { + mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled()); + mTBShowThreadUpdates.setEnabled(true); + mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled()); + mTBShowHeapUpdates.setEnabled(true); + mTBHalt.setEnabled(true); + mTBCauseGc.setEnabled(true); + } else { + // list is empty, disable these + mTBShowThreadUpdates.setSelection(false); + mTBShowThreadUpdates.setEnabled(false); + mTBShowHeapUpdates.setSelection(false); + mTBShowHeapUpdates.setEnabled(false); + mTBHalt.setEnabled(false); + mTBCauseGc.setEnabled(false); + } + } + + /** + * Sent when a new {@link Device} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + * + * @see IUiSelectionListener + */ + public void selectionChanged(Device selectedDevice, Client selectedClient) { + if (mCurrentDevice != selectedDevice) { + mCurrentDevice = selectedDevice; + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.deviceSelected(mCurrentDevice); + } + } + + mEmulatorPanel.deviceSelected(mCurrentDevice); + mLogPanel.deviceSelected(mCurrentDevice); + if (mEventLogPanel != null) { + mEventLogPanel.deviceSelected(mCurrentDevice); + } + + if (mExplorer != null) { + mExplorer.switchDevice(mCurrentDevice); + } + } + + if (mCurrentClient != selectedClient) { + AndroidDebugBridge.getBridge().setSelectedClient(selectedClient); + mCurrentClient = selectedClient; + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.clientSelected(mCurrentClient); + } + } + + enableButtons(); + } + } +} diff --git a/ddms/app/src/resources/images/ddms-icon.png b/ddms/app/src/resources/images/ddms-icon.png Binary files differnew file mode 100644 index 0000000..167a83b --- /dev/null +++ b/ddms/app/src/resources/images/ddms-icon.png diff --git a/ddms/app/src/resources/images/ddms-logo.png b/ddms/app/src/resources/images/ddms-logo.png Binary files differnew file mode 100644 index 0000000..b4708b4 --- /dev/null +++ b/ddms/app/src/resources/images/ddms-logo.png diff --git a/ddms/libs/Android.mk b/ddms/libs/Android.mk new file mode 100644 index 0000000..c62c6d0 --- /dev/null +++ b/ddms/libs/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMSLIBS_LOCAL_DIR := $(call my-dir) +include $(DDMSLIBS_LOCAL_DIR)/ddmlib/Android.mk +include $(DDMSLIBS_LOCAL_DIR)/ddmuilib/Android.mk diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath new file mode 100644 index 0000000..fb50116 --- /dev/null +++ b/ddms/libs/ddmlib/.classpath @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/libs/ddmlib/.project b/ddms/libs/ddmlib/.project new file mode 100644 index 0000000..fea25c7 --- /dev/null +++ b/ddms/libs/ddmlib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddmlib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk new file mode 100644 index 0000000..a49bdd2 --- /dev/null +++ b/ddms/libs/ddmlib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +DDMLIB_LOCAL_DIR := $(call my-dir) +include $(DDMLIB_LOCAL_DIR)/src/Android.mk diff --git a/ddms/libs/ddmlib/src/Android.mk b/ddms/libs/ddmlib/src/Android.mk new file mode 100644 index 0000000..da07f97 --- /dev/null +++ b/ddms/libs/ddmlib/src/Android.mk @@ -0,0 +1,11 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := ddmlib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java new file mode 100644 index 0000000..42022fe --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java @@ -0,0 +1,714 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; + +/** + * Helper class to handle requests and connections to adb. + * <p/>{@link DebugBridgeServer} is the public API to connection to adb, while {@link AdbHelper} + * does the low level stuff. + * <p/>This currently uses spin-wait non-blocking I/O. A Selector would be more efficient, + * but seems like overkill for what we're doing here. + */ +final class AdbHelper { + + // public static final long kOkay = 0x59414b4fL; + // public static final long kFail = 0x4c494146L; + + static final int WAIT_TIME = 5; // spin-wait sleep, in ms + + public static final int STD_TIMEOUT = 5000; // standard delay, in ms + + static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + /** do not instantiate */ + private AdbHelper() { + } + + /** + * Response from ADB. + */ + static class AdbResponse { + public AdbResponse() { + // ioSuccess = okay = timeout = false; + message = ""; + } + + public boolean ioSuccess; // read all expected data, no timeoutes + + public boolean okay; // first 4 bytes in response were "OKAY"? + + public boolean timeout; // TODO: implement + + public String message; // diagnostic string + } + + /** + * Create and connect a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param devicePort the port we're opening + */ + public static SocketChannel open(InetSocketAddress adbSockAddr, + Device device, int devicePort) throws IOException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createAdbForwardRequest(null, devicePort); + // Log.hexDump(req); + + if (write(adbChan, req) == false) + throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$ + + AdbResponse resp = readAdbResponse(adbChan, false); + if (!resp.okay) + throw new IOException("connection request rejected"); //$NON-NLS-1$ + + adbChan.configureBlocking(true); + } catch (IOException ioe) { + adbChan.close(); + throw ioe; + } + + return adbChan; + } + + /** + * Creates and connects a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param pid the process pid to connect to. + */ + public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr, + Device device, int pid) throws IOException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createJdwpForwardRequest(pid); + // Log.hexDump(req); + + if (write(adbChan, req) == false) + throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$ + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) + throw new IOException("connection request rejected: " + resp.message); //$NON-NLS-1$ + + adbChan.configureBlocking(true); + } catch (IOException ioe) { + adbChan.close(); + throw ioe; + } + + return adbChan; + } + + /** + * Creates a port forwarding request for adb. This returns an array + * containing "####tcp:{port}:{addStr}". + * @param addrStr the host. Can be null. + * @param port the port on the device. This does not need to be numeric. + */ + private static byte[] createAdbForwardRequest(String addrStr, int port) { + String reqStr; + + if (addrStr == null) + reqStr = "tcp:" + port; + else + reqStr = "tcp:" + port + ":" + addrStr; + return formAdbRequest(reqStr); + } + + /** + * Creates a port forwarding request to a jdwp process. This returns an array + * containing "####jwdp:{pid}". + * @param pid the jdwp process pid on the device. + */ + private static byte[] createJdwpForwardRequest(int pid) { + String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$ + return formAdbRequest(reqStr); + } + + /** + * Create an ASCII string preceeded by four hex digits. The opening "####" + * is the length of the rest of the string, encoded as ASCII hex (case + * doesn't matter). "port" and "host" are what we want to forward to. If + * we're on the host side connecting into the device, "addrStr" should be + * null. + */ + static byte[] formAdbRequest(String req) { + String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$ + byte[] result; + try { + result = resultStr.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + return null; + } + assert result.length == req.length() + 4; + return result; + } + + /** + * Reads the response from ADB after a command. + * @param chan The socket channel that is connected to adb. + * @param readDiagString If true, we're expecting an OKAY response to be + * followed by a diagnostic string. Otherwise, we only expect the + * diagnostic string to follow a FAIL. + */ + static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString) + throws IOException { + + AdbResponse resp = new AdbResponse(); + + byte[] reply = new byte[4]; + if (read(chan, reply) == false) { + return resp; + } + resp.ioSuccess = true; + + if (isOkay(reply)) { + resp.okay = true; + } else { + readDiagString = true; // look for a reason after the FAIL + resp.okay = false; + } + + // not a loop -- use "while" so we can use "break" + while (readDiagString) { + // length string is in next 4 bytes + byte[] lenBuf = new byte[4]; + if (read(chan, lenBuf) == false) { + Log.w("ddms", "Expected diagnostic string not found"); + break; + } + + String lenStr = replyToString(lenBuf); + + int len; + try { + len = Integer.parseInt(lenStr, 16); + } catch (NumberFormatException nfe) { + Log.w("ddms", "Expected digits, got '" + lenStr + "': " + + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " " + + lenBuf[3]); + Log.w("ddms", "reply was " + replyToString(reply)); + break; + } + + byte[] msg = new byte[len]; + if (read(chan, msg) == false) { + Log.w("ddms", "Failed reading diagnostic string, len=" + len); + break; + } + + resp.message = replyToString(msg); + Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='" + + resp.message + "'"); + + break; + } + + return resp; + } + + /** + * Retrieve the frame buffer from the device. + */ + public static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device) + throws IOException { + + RawImage imageParams = new RawImage(); + byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$ + byte[] nudge = { + 0 + }; + byte[] reply; + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + if (write(adbChan, request) == false) + throw new IOException("failed asking for frame buffer"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.w("ddms", "Got timeout or unhappy response from ADB fb req: " + + resp.message); + adbChan.close(); + return null; + } + + reply = new byte[16]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got partial reply from ADB fb:"); + Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length); + adbChan.close(); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + imageParams.bpp = buf.getInt(); + imageParams.size = buf.getInt(); + imageParams.width = buf.getInt(); + imageParams.height = buf.getInt(); + + Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" + + imageParams.size + ", width=" + imageParams.width + + ", height=" + imageParams.height); + + if (write(adbChan, nudge) == false) + throw new IOException("failed nudging"); + + reply = new byte[imageParams.size]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got truncated reply from ADB fb data"); + adbChan.close(); + return null; + } + imageParams.data = reply; + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return imageParams; + } + + /** + * Execute a command on the device and retrieve the output. The output is + * handed to "rcvr" as it arrives. + */ + public static void executeRemoteCommand(InetSocketAddress adbSockAddr, + String command, Device device, IShellOutputReceiver rcvr) + throws IOException { + Log.v("ddms", "execute: running " + command); + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$ + if (write(adbChan, request) == false) + throw new IOException("failed submitting shell command"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message); + throw new IOException("sad result from adb: " + resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + Log.v("ddms", "execute: cancelled"); + break; + } + + count = adbChan.read(buf); + if (count < 0) { + // we're at the end, we flush the output + rcvr.flush(); + Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + + count); + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + Log.v("ddms", "execute: returning"); + } + } + + /** + * Runs the Event log service on the {@link Device}, and provides its output to the + * {@link LogReceiver}. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws IOException + */ + public static void runEventLogService(InetSocketAddress adbSockAddr, Device device, + LogReceiver rcvr) throws IOException { + runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$ + } + + /** + * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param logName the name of the log file to output + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws IOException + */ + public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName, + LogReceiver rcvr) throws IOException { + SocketChannel adbChan = null; + + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("log:" + logName); + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the log command"); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected log command: " + resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + break; + } + + count = adbChan.read(buf); + if (count < 0) { + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } + + /** + * Creates a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to do the port fowarding + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return <code>true</code> if success. + * @throws IOException + */ + public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort, + int remotePort) throws IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.serialNumber, localPort, remotePort)); + + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return true; + } + + /** + * Remove a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to remove the port fowarding + * @param localPort the local port of the forward + * @param remotePort the remote port. + * @return <code>true</code> if success. + * @throws IOException + */ + public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort, + int remotePort) throws IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.serialNumber, localPort, remotePort)); + + if (!write(adbChan, request)) { + throw new IOException("failed to submit the remove forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return true; + } + + /** + * Checks to see if the first four bytes in "reply" are OKAY. + */ + static boolean isOkay(byte[] reply) { + return reply[0] == (byte)'O' && reply[1] == (byte)'K' + && reply[2] == (byte)'A' && reply[3] == (byte)'Y'; + } + + /** + * Converts an ADB reply to a string. + */ + static String replyToString(byte[] reply) { + String result; + try { + result = new String(reply, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + result = ""; + } + return result; + } + + /** + * Reads from the socket until the array is filled, or no more data is coming (because + * the socket closed or the timeout expired). + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @return "true" if all data was read. + * @throws IOException + */ + static boolean read(SocketChannel chan, byte[] data) { + try { + read(chan, data, -1, STD_TIMEOUT); + } catch (IOException e) { + Log.d("ddms", "readAll: IOException: " + e.getMessage()); + return false; + } + + return true; + } + + /** + * Reads from the socket until the array is filled, the optional length + * is reached, or no more data is coming (because the socket closed or the + * timeout expired). After "timeout" milliseconds since the + * previous successful read, this will return whether or not new data has + * been found. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @param length the length to read or -1 to fill the data buffer completely + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws IOException + */ + static void read(SocketChannel chan, byte[] data, int length, int timeout) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.read(buf); + if (count < 0) { + Log.d("ddms", "read: channel EOF"); + throw new IOException("EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.i("ddms", "read: timeout"); + throw new IOException("timeout"); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * Write until all data in "data" is written or the connection fails. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @return "true" if all data was written. + */ + static boolean write(SocketChannel chan, byte[] data) { + try { + write(chan, data, -1, STD_TIMEOUT); + } catch (IOException e) { + Log.e("ddms", e); + return false; + } + + return true; + } + + /** + * Write until all data in "data" is written, the optional length is reached, + * the timeout expires, or the connection fails. Returns "true" if all + * data was written. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @param length the length to write or -1 to send the whole buffer. + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws IOException + */ + static void write(SocketChannel chan, byte[] data, int length, int timeout) + throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.write(buf); + if (count < 0) { + Log.d("ddms", "write: channel EOF"); + throw new IOException("channel EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.i("ddms", "write: timeout"); + throw new IOException("timeout"); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * tells adb to talk to a specific device + * + * @param adbChan the socket connection to adb + * @param device The device to talk to. + * @throws IOException + */ + static void setDevice(SocketChannel adbChan, Device device) + throws IOException { + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + if (device != null) { + String msg = "host:transport:" + device.serialNumber; //$NON-NLS-1$ + byte[] device_query = formAdbRequest(msg); + + if (write(adbChan, device_query) == false) + throw new IOException("failed submitting device (" + device + + ") request to ADB"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) + throw new IOException("device (" + device + + ") request rejected: " + resp.message); + } + + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java new file mode 100644 index 0000000..c6d4b50 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Holds an Allocation information. + */ +public class AllocationInfo implements Comparable<AllocationInfo>, IStackTraceInfo { + private String mAllocatedClass; + private int mAllocationSize; + private short mThreadId; + private StackTraceElement[] mStackTrace; + + /* + * Simple constructor. + */ + AllocationInfo(String allocatedClass, int allocationSize, + short threadId, StackTraceElement[] stackTrace) { + mAllocatedClass = allocatedClass; + mAllocationSize = allocationSize; + mThreadId = threadId; + mStackTrace = stackTrace; + } + + /** + * Returns the name of the allocated class. + */ + public String getAllocatedClass() { + return mAllocatedClass; + } + + /** + * Returns the size of the allocation. + */ + public int getSize() { + return mAllocationSize; + } + + /** + * Returns the id of the thread that performed the allocation. + */ + public short getThreadId() { + return mThreadId; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + public StackTraceElement[] getStackTrace() { + return mStackTrace; + } + + public int compareTo(AllocationInfo otherAlloc) { + return otherAlloc.mAllocationSize - mAllocationSize; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java new file mode 100644 index 0000000..795bf88 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java @@ -0,0 +1,1050 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.Thread.State; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A connection to the host-side android debug bridge (adb) + * <p/>This is the central point to communicate with any devices, emulators, or the applications + * running on them. + * <p/><b>{@link #init(boolean)} must be called before anything is done.</b> + */ +public final class AndroidDebugBridge { + + /* + * Minimum and maximum version of adb supported. This correspond to + * ADB_SERVER_VERSION found in //device/tools/adb/adb.h + */ + + private final static int ADB_VERSION_MICRO_MIN = 20; + private final static int ADB_VERSION_MICRO_MAX = -1; + + private final static Pattern sAdbVersion = Pattern.compile( + "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$ + + private final static String ADB = "adb"; //$NON-NLS-1$ + private final static String DDMS = "ddms"; //$NON-NLS-1$ + + // Where to find the ADB bridge. + final static String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$ + final static int ADB_PORT = 5037; + + static InetAddress sHostAddr; + static InetSocketAddress sSocketAddr; + + static { + // built-in local address/port for ADB. + try { + sHostAddr = InetAddress.getByName(ADB_HOST); + sSocketAddr = new InetSocketAddress(sHostAddr, ADB_PORT); + } catch (UnknownHostException e) { + + } + } + + private static AndroidDebugBridge sThis; + private static boolean sClientSupport; + + /** Full path to adb. */ + private String mAdbOsLocation = null; + + private boolean mVersionCheck; + + private boolean mStarted = false; + + private DeviceMonitor mDeviceMonitor; + + private final static ArrayList<IDebugBridgeChangeListener> sBridgeListeners = + new ArrayList<IDebugBridgeChangeListener>(); + private final static ArrayList<IDeviceChangeListener> sDeviceListeners = + new ArrayList<IDeviceChangeListener>(); + private final static ArrayList<IClientChangeListener> sClientListeners = + new ArrayList<IClientChangeListener>(); + + // lock object for synchronization + private static final Object sLock = sBridgeListeners; + + /** + * Classes which implement this interface provide a method that deals + * with {@link AndroidDebugBridge} changes. + */ + public interface IDebugBridgeChangeListener { + /** + * Sent when a new {@link AndroidDebugBridge} is connected. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + */ + public void bridgeChanged(AndroidDebugBridge bridge); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Device} addition, deletion, and changes. + */ + public interface IDeviceChangeListener { + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceConnected(Device device); + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceDisconnected(Device device); + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask describing what changed. It can contain any of the following + * values: {@link Device#CHANGE_BUILD_INFO}, {@link Device#CHANGE_STATE}, + * {@link Device#CHANGE_CLIENT_LIST} + */ + public void deviceChanged(Device device, int changeMask); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Client} changes. + */ + public interface IClientChangeListener { + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + */ + public void clientChanged(Client client, int changeMask); + } + + /** + * Initializes the <code>ddm</code> library. + * <p/>This must be called once <b>before</b> any call to + * {@link #createBridge(String, boolean)}. + * <p>The library can be initialized in 2 ways: + * <ul> + * <li>Mode 1: <var>clientSupport</var> == <code>true</code>.<br>The library monitors the + * devices and the applications running on them. It will connect to each application, as a + * debugger of sort, to be able to interact with them through JDWP packets.</li> + * <li>Mode 2: <var>clientSupport</var> == <code>false</code>.<br>The library only monitors + * devices. The applications are left untouched, letting other tools built on + * <code>ddmlib</code> to connect a debugger to them.</li> + * </ul> + * <p/><b>Only one tool can run in mode 1 at the same time.</b> + * <p/>Note that mode 1 does not prevent debugging of applications running on devices. Mode 1 + * lets debuggers connect to <code>ddmlib</code> which acts as a proxy between the debuggers and + * the applications to debug. See {@link Client#getDebuggerListenPort()}. + * <p/>The preferences of <code>ddmlib</code> should also be initialized with whatever default + * values were changed from the default values. + * <p/>When the application quits, {@link #terminate()} should be called. + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * @see AndroidDebugBridge#createBridge(String, boolean) + * @see DdmPreferences + */ + public static void init(boolean clientSupport) { + sClientSupport = clientSupport; + + MonitorThread monitorThread = MonitorThread.createInstance(); + monitorThread.start(); + + HandleHello.register(monitorThread); + HandleAppName.register(monitorThread); + HandleTest.register(monitorThread); + HandleThread.register(monitorThread); + HandleHeap.register(monitorThread); + HandleWait.register(monitorThread); + } + + /** + * Terminates the ddm library. This must be called upon application termination. + */ + public static void terminate() { + // kill the monitoring services + if (sThis != null && sThis.mDeviceMonitor != null) { + sThis.mDeviceMonitor.stop(); + sThis.mDeviceMonitor = null; + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.quit(); + } + } + + /** + * Returns whether the ddmlib is setup to support monitoring and interacting with + * {@link Client}s running on the {@link Device}s. + */ + static boolean getClientSupport() { + return sClientSupport; + } + + /** + * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable. + * <p/>This bridge will expect adb to be running. It will not be able to start/stop/restart + * adb. + * <p/>If a bridge has already been started, it is directly returned with no changes (similar + * to calling {@link #getBridge()}). + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge() { + synchronized (sLock) { + if (sThis != null) { + return sThis; + } + + try { + sThis = new AndroidDebugBridge(); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + + /** + * Creates a new debug bridge from the location of the command line tool. + * <p/> + * Any existing server will be disconnected, unless the location is the same and + * <code>forceNewBridge</code> is set to false. + * @param osLocation the location of the command line tool 'adb' + * @param forceNewBridge force creation of a new bridge even if one with the same location + * already exists. + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) { + synchronized (sLock) { + if (sThis != null) { + if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) && + forceNewBridge == false) { + return sThis; + } else { + // stop the current server + sThis.stop(); + } + } + + try { + sThis = new AndroidDebugBridge(osLocation); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + /** + * Returns the current debug bridge. Can be <code>null</code> if none were created. + */ + public static AndroidDebugBridge getBridge() { + return sThis; + } + + /** + * Disconnects the current debug bridge, and destroy the object. + * <p/> + * A new object will have to be created with {@link #createBridge(String, boolean)}. + */ + public static void disconnectBridge() { + synchronized (sLock) { + if (sThis != null) { + sThis.stop(); + sThis = null; + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners. + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined + * in the {@link IDebugBridgeChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + if (sBridgeListeners.contains(listener) == false) { + sBridgeListeners.add(listener); + if (sThis != null) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is started. + * @param listener The listener which should no longer be notified. + */ + public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + sBridgeListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Device} + * is connected, disconnected, or when its properties or its {@link Client} list changed, + * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + if (sDeviceListeners.contains(listener) == false) { + sDeviceListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Device} is connected, disconnected, or when its properties or its {@link Client} + * list changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + sDeviceListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Client} + * property changed, by sending it one of the messages defined in the + * {@link IClientChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + if (sClientListeners.contains(listener) == false) { + sClientListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Client} property changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + sClientListeners.remove(listener); + } + } + + + /** + * Returns the devices. + * @see #hasInitialDeviceList() + */ + public Device[] getDevices() { + synchronized (sLock) { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getDevices(); + } + } + + return new Device[0]; + } + + /** + * Returns whether the bridge has acquired the initial list from adb after being created. + * <p/>Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will + * generally result in an empty list. This is due to the internal asynchronous communication + * mechanism with <code>adb</code> that does not guarantee that the {@link Device} list has been + * built before the call to {@link #getDevices()}. + * <p/>The recommended way to get the list of {@link Device} objects is to create a + * {@link IDeviceChangeListener} object. + */ + public boolean hasInitialDeviceList() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.hasInitialDeviceList(); + } + + return false; + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + public void setSelectedClient(Client selectedClient) { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(selectedClient); + } + } + + /** + * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon. + */ + public boolean isConnected() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (mDeviceMonitor != null && monitorThread != null) { + return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED; + } + return false; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect + * to the adb daemon. + */ + public int getConnectionAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getConnectionAttemptCount(); + } + return -1; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart + * the adb daemon. + */ + public int getRestartAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getRestartAttemptCount(); + } + return -1; + } + + /** + * Creates a new bridge. + * @param osLocation the location of the command line tool + * @throws InvalidParameterException + */ + private AndroidDebugBridge(String osLocation) throws InvalidParameterException { + if (osLocation == null || osLocation.length() == 0) { + throw new InvalidParameterException(); + } + mAdbOsLocation = osLocation; + + checkAdbVersion(); + } + + /** + * Creates a new bridge not linked to any particular adb executable. + */ + private AndroidDebugBridge() { + } + + /** + * Queries adb for its version number and checks it against {@link #MIN_VERSION_NUMBER} and + * {@link #MAX_VERSION_NUMBER} + */ + private void checkAdbVersion() { + // default is bad check + mVersionCheck = false; + + if (mAdbOsLocation == null) { + return; + } + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "version"; //$NON-NLS-1$ + Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation)); //$NON-NLS-1$ + Process process = Runtime.getRuntime().exec(command); + + ArrayList<String> errorOutput = new ArrayList<String>(); + ArrayList<String> stdOutput = new ArrayList<String>(); + int status = grabProcessOutput(process, errorOutput, stdOutput, + true /* waitForReaders */); + + if (status != 0) { + StringBuilder builder = new StringBuilder("'adb version' failed!"); //$NON-NLS-1$ + for (String error : errorOutput) { + builder.append('\n'); + builder.append(error); + } + Log.logAndDisplay(LogLevel.ERROR, "adb", builder.toString()); + } + + // check both stdout and stderr + boolean versionFound = false; + for (String line : stdOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + if (!versionFound) { + for (String line : errorOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + } + + if (!versionFound) { + // if we get here, we failed to parse the output. + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to parse the output of 'adb version'"); //$NON-NLS-1$ + } + + } catch (IOException e) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to get the adb version: " + e.getMessage()); //$NON-NLS-1$ + } catch (InterruptedException e) { + } finally { + + } + } + + /** + * Scans a line resulting from 'adb version' for a potential version number. + * <p/> + * If a version number is found, it checks the version number against what is expected + * by this version of ddms. + * <p/> + * Returns true when a version number has been found so that we can stop scanning, + * whether the version number is in the acceptable range or not. + * + * @param line The line to scan. + * @return True if a version number was found (whether it is acceptable or not). + */ + private boolean scanVersionLine(String line) { + if (line != null) { + Matcher matcher = sAdbVersion.matcher(line); + if (matcher.matches()) { + int majorVersion = Integer.parseInt(matcher.group(1)); + int minorVersion = Integer.parseInt(matcher.group(2)); + int microVersion = Integer.parseInt(matcher.group(3)); + + // check only the micro version for now. + if (microVersion < ADB_VERSION_MICRO_MIN) { + String message = String.format( + "Required minimum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MIN, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else if (ADB_VERSION_MICRO_MAX != -1 && + microVersion > ADB_VERSION_MICRO_MAX) { + String message = String.format( + "Required maximum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MAX, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else { + mVersionCheck = true; + } + + return true; + } + } + return false; + } + + /** + * Starts the debug bridge. + * @return true if success. + */ + boolean start() { + if (mAdbOsLocation != null && (mVersionCheck == false || startAdb() == false)) { + return false; + } + + mStarted = true; + + // now that the bridge is connected, we start the underlying services. + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + + return true; + } + + /** + * Kills the debug bridge. + * @return true if success + */ + boolean stop() { + // if we haven't started we return false; + if (mStarted == false) { + return false; + } + + // kill the monitoring services + mDeviceMonitor.stop(); + mDeviceMonitor = null; + + if (stopAdb() == false) { + return false; + } + + mStarted = false; + return true; + } + + /** + * Restarts adb, but not the services around it. + * @return true if success. + */ + public boolean restart() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + if (mVersionCheck == false) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$ + return false; + } + synchronized (this) { + stopAdb(); + + boolean restart = startAdb(); + + if (restart && mDeviceMonitor == null) { + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + } + + return restart; + } + } + + /** + * Notify the listener of a new {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the new <code>Device</code>. + * @see #getLock() + */ + void deviceConnected(Device device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceConnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a disconnected {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the disconnected <code>Device</code>. + * @see #getLock() + */ + void deviceDisconnected(Device device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceDisconnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified <code>Device</code>. + * @see #getLock() + */ + void deviceChanged(Device device, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceChanged(device, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Client}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified <code>Client</code>. + * @param changeMask the mask indicating what changed in the <code>Client</code> + * @see #getLock() + */ + void clientChanged(Client client, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IClientChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sClientListeners.toArray( + new IClientChangeListener[sClientListeners.size()]); + + } + + // Notify the listeners + for (IClientChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.clientChanged(client, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Returns the {@link DeviceMonitor} object. + */ + DeviceMonitor getDeviceMonitor() { + return mDeviceMonitor; + } + + /** + * Starts the adb host side server. + * @return true if success + */ + synchronized boolean startAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "start-server"; //$NON-NLS-1$ + Log.d(DDMS, + String.format("Launching '%1$s %2$s' to ensure ADB is running.", //$NON-NLS-1$ + mAdbOsLocation, command[1])); + proc = Runtime.getRuntime().exec(command); + + ArrayList<String> errorOutput = new ArrayList<String>(); + ArrayList<String> stdOutput = new ArrayList<String>(); + status = grabProcessOutput(proc, errorOutput, stdOutput, + false /* waitForReaders */); + + } catch (IOException ioe) { + Log.d(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + Log.d(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb start-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb start-server' succeeded"); //$NON-NLS-1$ + + return true; + } + + /** + * Stops the adb host side server. + * @return true if success + */ + private synchronized boolean stopAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "kill-server"; //$NON-NLS-1$ + proc = Runtime.getRuntime().exec(command); + status = proc.waitFor(); + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb kill-server' succeeded"); //$NON-NLS-1$ + return true; + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both <b>must</b> be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param displayStdOut If true this will display stdout as well + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput, + final ArrayList<String> stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + Log.e(ADB, line); + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + Log.d(ADB, line); + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Returns the singleton lock used by this class to protect any access to the listener. + * <p/> + * This includes adding/removing listeners, but also notifying listeners of new bridges, + * devices, and clients. + */ + static Object getLock() { + return sLock; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java new file mode 100644 index 0000000..129b312 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java @@ -0,0 +1,35 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddmlib; + +/** + * Thrown if the contents of a packet are bad. + */ +@SuppressWarnings("serial") +class BadPacketException extends RuntimeException { + public BadPacketException() + { + super(); + } + + public BadPacketException(String msg) + { + super(msg); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java new file mode 100644 index 0000000..441b024 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Subclass this with a class that handles one or more chunk types. + */ +abstract class ChunkHandler { + + public static final int CHUNK_HEADER_LEN = 8; // 4-byte type, 4-byte len + public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN; + + public static final int CHUNK_FAIL = type("FAIL"); + + ChunkHandler() {} + + /** + * Client is ready. The monitor thread calls this method on all + * handlers when the client is determined to be DDM-aware (usually + * after receiving a HELO response.) + * + * The handler can use this opportunity to initialize client-side + * activity. Because there's a fair chance we'll want to send a + * message to the client, this method can throw an IOException. + */ + abstract void clientReady(Client client) throws IOException; + + /** + * Client has gone away. Can be used to clean up any resources + * associated with this client connection. + */ + abstract void clientDisconnected(Client client); + + /** + * Handle an incoming chunk. The data, of chunk type "type", begins + * at the start of "data" and continues to data.limit(). + * + * If "isReply" is set, then "msgId" will be the ID of the request + * we sent to the client. Otherwise, it's the ID generated by the + * client for this event. Note that it's possible to receive chunks + * in reply packets for which we are not registered. + * + * The handler may not modify the contents of "data". + */ + abstract void handleChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId); + + /** + * Handle chunks not recognized by handlers. The handleChunk() method + * in sub-classes should call this if the chunk type isn't recognized. + */ + protected void handleUnknownChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId) { + if (type == CHUNK_FAIL) { + int errorCode, msgLen; + String msg; + + errorCode = data.getInt(); + msgLen = data.getInt(); + msg = getString(data, msgLen); + Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg); + } else { + Log.w("ddms", "WARNING: received unknown chunk " + name(type) + + ": len=" + data.limit() + ", reply=" + isReply + + ", msgId=0x" + Integer.toHexString(msgId)); + } + Log.w("ddms", " client " + client + ", handler " + this); + } + + + /** + * Utility function to copy a String out of a ByteBuffer. + * + * This is here because multiple chunk handlers can make use of it, + * and there's nowhere better to put it. + */ + static String getString(ByteBuffer buf, int len) { + char[] data = new char[len]; + for (int i = 0; i < len; i++) + data[i] = buf.getChar(); + return new String(data); + } + + /** + * Utility function to copy a String into a ByteBuffer. + */ + static void putString(ByteBuffer buf, String str) { + int len = str.length(); + for (int i = 0; i < len; i++) + buf.putChar(str.charAt(i)); + } + + /** + * Convert a 4-character string to a 32-bit type. + */ + static int type(String typeName) { + int val = 0; + + if (typeName.length() != 4) { + Log.e("ddms", "Type name must be 4 letter long"); + throw new RuntimeException("Type name must be 4 letter long"); + } + + for (int i = 0; i < 4; i++) { + val <<= 8; + val |= (byte) typeName.charAt(i); + } + + return val; + } + + /** + * Convert an integer type to a 4-character string. + */ + static String name(int type) { + char[] ascii = new char[4]; + + ascii[0] = (char) ((type >> 24) & 0xff); + ascii[1] = (char) ((type >> 16) & 0xff); + ascii[2] = (char) ((type >> 8) & 0xff); + ascii[3] = (char) (type & 0xff); + + return new String(ascii); + } + + /** + * Allocate a ByteBuffer with enough space to hold the JDWP packet + * header and one chunk header in addition to the demands of the + * chunk being created. + * + * "maxChunkLen" indicates the size of the chunk contents only. + */ + static ByteBuffer allocBuffer(int maxChunkLen) { + ByteBuffer buf = + ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen); + buf.order(CHUNK_ORDER); + return buf; + } + + /** + * Return the slice of the JDWP packet buffer that holds just the + * chunk data. + */ + static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) { + ByteBuffer slice; + + assert jdwpBuf.position() == 0; + + jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN); + slice = jdwpBuf.slice(); + slice.order(CHUNK_ORDER); + jdwpBuf.position(0); + + return slice; + } + + /** + * Write the chunk header at the start of the chunk. + * + * Pass in the byte buffer returned by JdwpPacket.getPayload(). + */ + static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) { + ByteBuffer buf = packet.getPayload(); + + buf.putInt(0x00, type); + buf.putInt(0x04, chunkLen); + + packet.finishPacket(CHUNK_HEADER_LEN + chunkLen); + } + + /** + * Check that the client is opened with the proper debugger port for the + * specified application name, and if not, reopen it. + * @param client + * @param uiThread + * @param appName + * @return + */ + protected static Client checkDebuggerPortForAppName(Client client, String appName) { + IDebugPortProvider provider = DebugPortManager.getProvider(); + if (provider != null) { + Device device = client.getDevice(); + int newPort = provider.getPort(device, appName); + + if (newPort != IDebugPortProvider.NO_STATIC_PORT && + newPort != client.getDebuggerListenPort()) { + + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + DeviceMonitor deviceMonitor = bridge.getDeviceMonitor(); + if (deviceMonitor != null) { + deviceMonitor.addClientToDropAndReopen(client, newPort); + client = null; + } + } + } + } + + return client; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java new file mode 100644 index 0000000..866d578 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java @@ -0,0 +1,768 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.HashMap; + +/** + * This represents a single client, usually a DAlvik VM process. + * <p/>This class gives access to basic client information, as well as methods to perform actions + * on the client. + * <p/>More detailed information, usually updated in real time, can be access through the + * {@link ClientData} class. Each <code>Client</code> object has its own <code>ClientData</code> + * accessed through {@link #getClientData()}. + */ +public class Client { + + private static final int SERVER_PROTOCOL_VERSION = 1; + + /** Client change bit mask: application name change */ + public static final int CHANGE_NAME = 0x0001; + /** Client change bit mask: debugger interest change */ + public static final int CHANGE_DEBUGGER_INTEREST = 0x0002; + /** Client change bit mask: debugger port change */ + public static final int CHANGE_PORT = 0x0004; + /** Client change bit mask: thread update flag change */ + public static final int CHANGE_THREAD_MODE = 0x0008; + /** Client change bit mask: thread data updated */ + public static final int CHANGE_THREAD_DATA = 0x0010; + /** Client change bit mask: heap update flag change */ + public static final int CHANGE_HEAP_MODE = 0x0020; + /** Client change bit mask: head data updated */ + public static final int CHANGE_HEAP_DATA = 0x0040; + /** Client change bit mask: native heap data updated */ + public static final int CHANGE_NATIVE_HEAP_DATA = 0x0080; + /** Client change bit mask: thread stack trace updated */ + public static final int CHANGE_THREAD_STACKTRACE = 0x0100; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATIONS = 0x0200; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATION_STATUS = 0x0400; + + /** Client change bit mask: combination of {@link Client#CHANGE_NAME}, + * {@link Client#CHANGE_DEBUGGER_INTEREST}, and {@link Client#CHANGE_PORT}. + */ + public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_INTEREST | CHANGE_PORT; + + private SocketChannel mChan; + + // debugger we're associated with, if any + private Debugger mDebugger; + private int mDebuggerListenPort; + + // list of IDs for requests we have sent to the client + private HashMap<Integer,ChunkHandler> mOutstandingReqs; + + // chunk handlers stash state data in here + private ClientData mClientData; + + // User interface state. Changing the value causes a message to be + // sent to the client. + private boolean mThreadUpdateEnabled; + private boolean mHeapUpdateEnabled; + + /* + * Read/write buffers. We can get large quantities of data from the + * client, e.g. the response to a "give me the list of all known classes" + * request from the debugger. Requests from the debugger, and from us, + * are much smaller. + * + * Pass-through debugger traffic is sent without copying. "mWriteBuffer" + * is only used for data generated within Client. + */ + private static final int INITIAL_BUF_SIZE = 2*1024; + private static final int MAX_BUF_SIZE = 200*1024*1024; + private ByteBuffer mReadBuffer; + + private static final int WRITE_BUF_SIZE = 256; + private ByteBuffer mWriteBuffer; + + private Device mDevice; + + private int mConnState; + + private static final int ST_INIT = 1; + private static final int ST_NOT_JDWP = 2; + private static final int ST_AWAIT_SHAKE = 10; + private static final int ST_NEED_DDM_PKT = 11; + private static final int ST_NOT_DDM = 12; + private static final int ST_READY = 13; + private static final int ST_ERROR = 20; + private static final int ST_DISCONNECTED = 21; + + + /** + * Create an object for a new client connection. + * + * @param device the device this client belongs to + * @param chan the connected {@link SocketChannel}. + * @param pid the client pid. + */ + Client(Device device, SocketChannel chan, int pid) { + mDevice = device; + mChan = chan; + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE); + + mOutstandingReqs = new HashMap<Integer,ChunkHandler>(); + + mConnState = ST_INIT; + + mClientData = new ClientData(pid); + + mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate(); + mHeapUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); + } + + /** + * Returns a string representation of the {@link Client} object. + */ + @Override + public String toString() { + return "[Client pid: " + mClientData.getPid() + "]"; + } + + /** + * Returns the {@link Device} on which this Client is running. + */ + public Device getDevice() { + return mDevice; + } + + /** + * Returns the debugger port for this client. + */ + public int getDebuggerListenPort() { + return mDebuggerListenPort; + } + + /** + * Returns <code>true</code> if the client VM is DDM-aware. + * + * Calling here is only allowed after the connection has been + * established. + */ + public boolean isDdmAware() { + switch (mConnState) { + case ST_INIT: + case ST_NOT_JDWP: + case ST_AWAIT_SHAKE: + case ST_NEED_DDM_PKT: + case ST_NOT_DDM: + case ST_ERROR: + case ST_DISCONNECTED: + return false; + case ST_READY: + return true; + default: + assert false; + return false; + } + } + + /** + * Returns <code>true</code> if a debugger is currently attached to the client. + */ + public boolean isDebuggerAttached() { + return mDebugger.isDebuggerAttached(); + } + + /** + * Return the Debugger object associated with this client. + */ + Debugger getDebugger() { + return mDebugger; + } + + /** + * Returns the {@link ClientData} object containing this client information. + */ + public ClientData getClientData() { + return mClientData; + } + + /** + * Forces the client to execute its garbage collector. + */ + public void executeGarbageCollector() { + try { + HandleHeap.sendHPGC(this); + } catch (IOException ioe) { + Log.w("ddms", "Send of HPGC message failed"); + // ignore + } + } + + /** + * Enables or disables the thread update. + * <p/>If <code>true</code> the VM will be able to send thread information. Thread information + * must be requested with {@link #requestThreadUpdate()}. + * @param enabled the enable flag. + */ + public void setThreadUpdateEnabled(boolean enabled) { + mThreadUpdateEnabled = enabled; + if (enabled == false) { + mClientData.clearThreads(); + } + + try { + HandleThread.sendTHEN(this, enabled); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + ioe.printStackTrace(); + } + + update(CHANGE_THREAD_MODE); + } + + /** + * Returns whether the thread update is enabled. + */ + public boolean isThreadUpdateEnabled() { + return mThreadUpdateEnabled; + } + + /** + * Sends a thread update request. This is asynchronous. + * <p/>The thread info can be accessed by {@link ClientData#getThreads()}. The notification + * that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code> + * containing the mask {@link #CHANGE_THREAD_DATA}. + */ + public void requestThreadUpdate() { + HandleThread.requestThreadUpdate(this); + } + + /** + * Sends a thread stack trace update request. This is asynchronous. + * <p/>The thread info can be accessed by {@link ClientData#getThreads()} and + * {@link ThreadInfo#getStackTrace()}. + * <p/>The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a <code>changeMask</code> containing the mask {@link #CHANGE_THREAD_STACKTRACE}. + */ + public void requestThreadStackTrace(int threadId) { + HandleThread.requestThreadStackCallRefresh(this, threadId); + } + + /** + * Enables or disables the heap update. + * <p/>If <code>true</code>, any GC will cause the client to send its heap information. + * <p/>The heap information can be accessed by {@link ClientData#getVmHeapData()}. + * <p/>The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a <code>changeMask</code> containing the value {@link #CHANGE_HEAP_DATA}. + * @param enabled the enable flag + */ + public void setHeapUpdateEnabled(boolean enabled) { + mHeapUpdateEnabled = enabled; + + try { + HandleHeap.sendHPIF(this, + enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER); + + HandleHeap.sendHPSG(this, + enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE, + HandleHeap.WHAT_MERGE); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + } + + update(CHANGE_HEAP_MODE); + } + + /** + * Returns whether the heap update is enabled. + * @see #setHeapUpdateEnabled(boolean) + */ + public boolean isHeapUpdateEnabled() { + return mHeapUpdateEnabled; + } + + /** + * Sends a native heap update request. this is asynchronous. + * <p/>The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code> + * containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}. + */ + public boolean requestNativeHeapInformation() { + try { + HandleNativeHeap.sendNHGT(this); + return true; + } catch (IOException e) { + Log.e("ddmlib", e); + } + + return false; + } + + /** + * Enables or disables the Allocation tracker for this client. + * <p/>If enabled, the VM will start tracking allocation informations. A call to + * {@link #requestAllocationDetails()} will make the VM sends the information about all the + * allocations that happened between the enabling and the request. + * @param enable + * @see #requestAllocationDetails() + */ + public void enableAllocationTracker(boolean enable) { + try { + HandleHeap.sendREAE(this, enable); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the enable status of the allocation tracking. + * This is asynchronous. + * <p/>The allocation status can be accessed by {@link ClientData#getAllocationStatus()}. + * The notification that the new status is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code> + * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}. + */ + public void requestAllocationStatus() { + try { + HandleHeap.sendREAQ(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the information about all the allocations that have + * happened since the call to {@link #enableAllocationTracker(boolean)} with <var>enable</var> + * set to <code>null</code>. This is asynchronous. + * <p/>The allocation information can be accessed by {@link ClientData#getAllocations()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code> + * containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}. + */ + public void requestAllocationDetails() { + try { + HandleHeap.sendREAL(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a kill message to the VM. + */ + public void kill() { + try { + HandleExit.sendEXIT(this, 1); + } catch (IOException ioe) { + Log.w("ddms", "Send of EXIT message failed"); + // ignore + } + } + + /** + * Registers the client with a Selector. + */ + void register(Selector sel) throws IOException { + if (mChan != null) { + mChan.register(sel, SelectionKey.OP_READ, this); + } + } + + /** + * Sets the client to accept debugger connection on the "selected debugger port". + * + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public void setAsSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(this); + } + } + + /** + * Returns whether this client is the current selected client, accepting debugger connection + * on the "selected debugger port". + * + * @see #setAsSelectedClient() + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public boolean isSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + return monitorThread.getSelectedClient() == this; + } + + return false; + } + + /** + * Tell the client to open a server socket channel and listen for + * connections on the specified port. + */ + void listenForDebugger(int listenPort) throws IOException { + mDebuggerListenPort = listenPort; + mDebugger = new Debugger(this, listenPort); + } + + /** + * Initiate the JDWP handshake. + * + * On failure, closes the socket and returns false. + */ + boolean sendHandshake() { + assert mWriteBuffer.position() == 0; + + try { + // assume write buffer can hold 14 bytes + JdwpPacket.putHandshake(mWriteBuffer); + int expectedLen = mWriteBuffer.position(); + mWriteBuffer.flip(); + if (mChan.write(mWriteBuffer) != expectedLen) + throw new IOException("partial handshake write"); + } + catch (IOException ioe) { + Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage()); + mConnState = ST_ERROR; + close(true /* notify */); + return false; + } + finally { + mWriteBuffer.clear(); + } + + mConnState = ST_AWAIT_SHAKE; + + return true; + } + + + /** + * Send a non-DDM packet to the client. + * + * Equivalent to sendAndConsume(packet, null). + */ + void sendAndConsume(JdwpPacket packet) throws IOException { + sendAndConsume(packet, null); + } + + /** + * Send a DDM packet to the client. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + */ + void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler) + throws IOException { + + if (mChan == null) { + // can happen for e.g. THST packets + Log.v("ddms", "Not sending packet -- client is closed"); + return; + } + + if (replyHandler != null) { + /* + * Add the ID to the list of outstanding requests. We have to do + * this before sending the packet, in case the response comes back + * before our thread returns from the packet-send function. + */ + addRequestId(packet.getId(), replyHandler); + } + + synchronized (mChan) { + try { + packet.writeAndConsume(mChan); + } + catch (IOException ioe) { + removeRequestId(packet.getId()); + throw ioe; + } + } + } + + /** + * Forward the packet to the debugger (if still connected to one). + * + * Consumes the packet. + */ + void forwardPacketToDebugger(JdwpPacket packet) + throws IOException { + + Debugger dbg = mDebugger; + + if (dbg == null) { + Log.i("ddms", "Discarding packet"); + packet.consume(); + } else { + dbg.sendAndConsume(packet); + } + } + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() + throws IOException, BufferOverflowException { + + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + Log.e("ddms", "Exceeded MAX_BUF_SIZE!"); + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2); + + // copy entire buffer to new buffer + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end of copied + + mReadBuffer = newBuffer; + } + + count = mChan.read(mReadBuffer); + if (count < 0) + throw new IOException("read failed"); + + if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this); + //Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(), + // mReadBuffer.arrayOffset(), mReadBuffer.position()); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. Upon receipt + * we send out the "HELO" message, which is why this can throw an + * IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + /* + * The first thing we get from the client is a response to our + * handshake. It doesn't look like a packet, so we have to + * handle it specially. + */ + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.i("ddms", + "Good handshake from client, sending HELO to " + mClientData.getPid()); + JdwpPacket.consumeHandshake(mReadBuffer); + mConnState = ST_NEED_DDM_PKT; + HandleHello.sendHELO(this, SERVER_PROTOCOL_VERSION); + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + Log.i("ddms", "Bad handshake from client"); + if (MonitorThread.getInstance().getRetryOnBadHandshake()) { + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + mDevice.getMonitor().addClientToDropAndReopen(this, + IDebugPortProvider.NO_STATIC_PORT); + } else { + // mark it as bad, close the socket, and don't retry + mConnState = ST_NOT_JDWP; + close(true /* notify */); + } + break; + case JdwpPacket.HANDSHAKE_NOTYET: + Log.i("ddms", "No handshake from client yet."); + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_NEED_DDM_PKT || + mConnState == ST_NOT_DDM || + mConnState == ST_READY) { + /* + * Normal packet traffic. + */ + if (mReadBuffer.position() != 0) { + if (Log.Config.LOGV) Log.v("ddms", + "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + /* + * Not expecting data when in this state. + */ + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /* + * Add the specified ID to the list of request IDs for which we await + * a response. + */ + private void addRequestId(int id, ChunkHandler handler) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Adding req 0x" + Integer.toHexString(id) +" to set"); + mOutstandingReqs.put(id, handler); + } + } + + /* + * Remove the specified ID from the list, if present. + */ + void removeRequestId(int id) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Removing req 0x" + Integer.toHexString(id) + " from set"); + mOutstandingReqs.remove(id); + } + + //Log.w("ddms", "Request " + Integer.toHexString(id) + // + " could not be removed from " + this); + } + + /** + * Determine whether this is a response to a request we sent earlier. + * If so, return the ChunkHandler responsible. + */ + ChunkHandler isResponseToUs(int id) { + + synchronized (mOutstandingReqs) { + ChunkHandler handler = mOutstandingReqs.get(id); + if (handler != null) { + if (Log.Config.LOGV) Log.v("ddms", + "Found 0x" + Integer.toHexString(id) + + " in request set - " + handler); + return handler; + } + } + + return null; + } + + /** + * An earlier request resulted in a failure. This is the expected + * response to a HELO message when talking to a non-DDM client. + */ + void packetFailed(JdwpPacket reply) { + if (mConnState == ST_NEED_DDM_PKT) { + Log.i("ddms", "Marking " + this + " as non-DDM client"); + mConnState = ST_NOT_DDM; + } else if (mConnState != ST_NOT_DDM) { + Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req"); + } + } + + /** + * The MonitorThread calls this when it sees a DDM request or reply. + * If we haven't seen a DDM packet before, we advance the state to + * ST_READY and return "false". Otherwise, just return true. + * + * The idea is to let the MonitorThread know when we first see a DDM + * packet, so we can send a broadcast to the handlers when a client + * connection is made. This method is synchronized so that we only + * send the broadcast once. + */ + synchronized boolean ddmSeen() { + if (mConnState == ST_NEED_DDM_PKT) { + mConnState = ST_READY; + return false; + } else if (mConnState != ST_READY) { + Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState); + } + return true; + } + + /** + * Close the client socket channel. If there is a debugger associated + * with us, close that too. + * + * Closing a channel automatically unregisters it from the selector. + * However, we have to iterate through the selector loop before it + * actually lets them go and allows the file descriptors to close. + * The caller is expected to manage that. + * @param notify Whether or not to notify the listeners of a change. + */ + void close(boolean notify) { + Log.i("ddms", "Closing " + this.toString()); + + mOutstandingReqs.clear(); + + try { + if (mChan != null) { + mChan.close(); + mChan = null; + } + + if (mDebugger != null) { + mDebugger.close(); + mDebugger = null; + } + } + catch (IOException ioe) { + Log.w("ddms", "failed to close " + this); + // swallow it -- not much else to do + } + + mDevice.removeClient(this, notify); + } + + /** + * Returns whether this {@link Client} has a valid connection to the application VM. + */ + public boolean isValid() { + return mChan != null; + } + + void update(int changeMask) { + mDevice.update(this, changeMask); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java new file mode 100644 index 0000000..2b46b6f --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + + +/** + * Contains the data of a {@link Client}. + */ +public class ClientData { + /* This is a place to stash data associated with a Client, such as thread + * states or heap data. ClientData maps 1:1 to Client, but it's a little + * cleaner if we separate the data out. + * + * Message handlers are welcome to stash arbitrary data here. + * + * IMPORTANT: The data here is written by HandleFoo methods and read by + * FooPanel methods, which run in different threads. All non-trivial + * access should be synchronized against the ClientData object. + */ + + + /** Temporary name of VM to be ignored. */ + private final static String PRE_INITIALIZED = "<pre-initialized>"; //$NON-NLS-1$ + + /** Debugger connection status: not waiting on one, not connected to one, but accepting + * new connections. This is the default value. */ + public static final int DEBUGGER_DEFAULT = 1; + /** + * Debugger connection status: the application's VM is paused, waiting for a debugger to + * connect to it before resuming. */ + public static final int DEBUGGER_WAITING = 2; + /** Debugger connection status : Debugger is connected */ + public static final int DEBUGGER_ATTACHED = 3; + /** Debugger connection status: The listening port for debugger connection failed to listen. + * No debugger will be able to connect. */ + public static final int DEBUGGER_ERROR = 4; + + /** + * Allocation tracking status: unknown. + * <p/>This happens right after a {@link Client} is discovered + * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query regarding + * its allocation tracking status. + * @see Client#requestAllocationStatus() + */ + public static final int ALLOCATION_TRACKING_UNKNOWN = -1; + /** + * Allocation tracking status: the {@link Client} is not tracking allocations. */ + public static final int ALLOCATION_TRACKING_OFF = 0; + /** + * Allocation tracking status: the {@link Client} is tracking allocations. */ + public static final int ALLOCATION_TRACKING_ON = 1; + + /** + * Name of the value representing the max size of the heap, in the {@link Map} returned by + * {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_MAX_SIZE_BYTES = "maxSizeInBytes"; // $NON-NLS-1$ + /** + * Name of the value representing the size of the heap, in the {@link Map} returned by + * {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_SIZE_BYTES = "sizeInBytes"; // $NON-NLS-1$ + /** + * Name of the value representing the number of allocated bytes of the heap, in the + * {@link Map} returned by {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_BYTES_ALLOCATED = "bytesAllocated"; // $NON-NLS-1$ + /** + * Name of the value representing the number of objects in the heap, in the {@link Map} + * returned by {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; // $NON-NLS-1$ + + // is this a DDM-aware client? + private boolean mIsDdmAware; + + // the client's process ID + private final int mPid; + + // Java VM identification string + private String mVmIdentifier; + + // client's self-description + private String mClientDescription; + + // how interested are we in a debugger? + private int mDebuggerInterest; + + // Thread tracking (THCR, THDE). + private TreeMap<Integer,ThreadInfo> mThreadMap; + + /** VM Heap data */ + private final HeapData mHeapData = new HeapData(); + /** Native Heap data */ + private final HeapData mNativeHeapData = new HeapData(); + + private HashMap<Integer, HashMap<String, Long>> mHeapInfoMap = + new HashMap<Integer, HashMap<String, Long>>(); + + + /** library map info. Stored here since the backtrace data + * is computed on a need to display basis. + */ + private ArrayList<NativeLibraryMapInfo> mNativeLibMapInfo = + new ArrayList<NativeLibraryMapInfo>(); + + /** Native Alloc info list */ + private ArrayList<NativeAllocationInfo> mNativeAllocationList = + new ArrayList<NativeAllocationInfo>(); + private int mNativeTotalMemory; + + private AllocationInfo[] mAllocations; + private int mAllocationStatus = ALLOCATION_TRACKING_UNKNOWN; + + /** + * Heap Information. + * <p/>The heap is composed of several {@link HeapSegment} objects. + * <p/>A call to {@link #isHeapDataComplete()} will indicate if the segments (available through + * {@link #getHeapSegments()}) represent the full heap. + */ + public static class HeapData { + private TreeSet<HeapSegment> mHeapSegments = new TreeSet<HeapSegment>(); + private boolean mHeapDataComplete = false; + private byte[] mProcessedHeapData; + private Map<Integer, ArrayList<HeapSegmentElement>> mProcessedHeapMap; + + /** + * Abandon the current list of heap segments. + */ + public synchronized void clearHeapData() { + /* Abandon the old segments instead of just calling .clear(). + * This lets the user hold onto the old set if it wants to. + */ + mHeapSegments = new TreeSet<HeapSegment>(); + mHeapDataComplete = false; + } + + /** + * Add raw HPSG chunk data to the list of heap segments. + * + * @param data The raw data from an HPSG chunk. + */ + synchronized void addHeapData(ByteBuffer data) { + HeapSegment hs; + + if (mHeapDataComplete) { + clearHeapData(); + } + + try { + hs = new HeapSegment(data); + } catch (BufferUnderflowException e) { + System.err.println("Discarding short HPSG data (length " + data.limit() + ")"); + return; + } + + mHeapSegments.add(hs); + } + + /** + * Called when all heap data has arrived. + */ + synchronized void sealHeapData() { + mHeapDataComplete = true; + } + + /** + * Returns whether the heap data has been sealed. + */ + public boolean isHeapDataComplete() { + return mHeapDataComplete; + } + + /** + * Get the collected heap data, if sealed. + * + * @return The list of heap segments if the heap data has been sealed, or null if it hasn't. + */ + public Collection<HeapSegment> getHeapSegments() { + if (isHeapDataComplete()) { + return mHeapSegments; + } + return null; + } + + /** + * Sets the processed heap data. + * + * @param heapData The new heap data (can be null) + */ + public void setProcessedHeapData(byte[] heapData) { + mProcessedHeapData = heapData; + } + + /** + * Get the processed heap data, if present. + * + * @return the processed heap data, or null. + */ + public byte[] getProcessedHeapData() { + return mProcessedHeapData; + } + + public void setProcessedHeapMap(Map<Integer, ArrayList<HeapSegmentElement>> heapMap) { + mProcessedHeapMap = heapMap; + } + + public Map<Integer, ArrayList<HeapSegmentElement>> getProcessedHeapMap() { + return mProcessedHeapMap; + } + + + } + + + /** + * Generic constructor. + */ + ClientData(int pid) { + mPid = pid; + + mDebuggerInterest = DEBUGGER_DEFAULT; + mThreadMap = new TreeMap<Integer,ThreadInfo>(); + } + + /** + * Returns whether the process is DDM-aware. + */ + public boolean isDdmAware() { + return mIsDdmAware; + } + + /** + * Sets DDM-aware status. + */ + void isDdmAware(boolean aware) { + mIsDdmAware = aware; + } + + /** + * Returns the process ID. + */ + public int getPid() { + return mPid; + } + + /** + * Returns the Client's VM identifier. + */ + public String getVmIdentifier() { + return mVmIdentifier; + } + + /** + * Sets VM identifier. + */ + void setVmIdentifier(String ident) { + mVmIdentifier = ident; + } + + /** + * Returns the client description. + * <p/>This is generally the name of the package defined in the + * <code>AndroidManifest.xml</code>. + * + * @return the client description or <code>null</code> if not the description was not yet + * sent by the client. + */ + public String getClientDescription() { + return mClientDescription; + } + + /** + * Sets client description. + * + * There may be a race between HELO and APNM. Rather than try + * to enforce ordering on the device, we just don't allow an empty + * name to replace a specified one. + */ + void setClientDescription(String description) { + if (mClientDescription == null && description.length() > 0) { + /* + * The application VM is first named <pre-initialized> before being assigned + * its real name. + * Depending on the timing, we can get an APNM chunk setting this name before + * another one setting the final actual name. So if we get a SetClientDescription + * with this value we ignore it. + */ + if (PRE_INITIALIZED.equals(description) == false) { + mClientDescription = description; + } + } + } + + /** + * Returns the debugger connection status. Possible values are {@link #DEBUGGER_DEFAULT}, + * {@link #DEBUGGER_WAITING}, {@link #DEBUGGER_ATTACHED}, and {@link #DEBUGGER_ERROR}. + */ + public int getDebuggerConnectionStatus() { + return mDebuggerInterest; + } + + /** + * Sets debugger connection status. + */ + void setDebuggerConnectionStatus(int val) { + mDebuggerInterest = val; + } + + /** + * Sets the current heap info values for the specified heap. + * + * @param heapId The heap whose info to update + * @param sizeInBytes The size of the heap, in bytes + * @param bytesAllocated The number of bytes currently allocated in the heap + * @param objectsAllocated The number of objects currently allocated in + * the heap + */ + // TODO: keep track of timestamp, reason + synchronized void setHeapInfo(int heapId, long maxSizeInBytes, + long sizeInBytes, long bytesAllocated, long objectsAllocated) { + HashMap<String, Long> heapInfo = new HashMap<String, Long>(); + heapInfo.put(HEAP_MAX_SIZE_BYTES, maxSizeInBytes); + heapInfo.put(HEAP_SIZE_BYTES, sizeInBytes); + heapInfo.put(HEAP_BYTES_ALLOCATED, bytesAllocated); + heapInfo.put(HEAP_OBJECTS_ALLOCATED, objectsAllocated); + mHeapInfoMap.put(heapId, heapInfo); + } + + /** + * Returns the {@link HeapData} object for the VM. + */ + public HeapData getVmHeapData() { + return mHeapData; + } + + /** + * Returns the {@link HeapData} object for the native code. + */ + HeapData getNativeHeapData() { + return mNativeHeapData; + } + + /** + * Returns an iterator over the list of known VM heap ids. + * <p/> + * The caller must synchronize on the {@link ClientData} object while iterating. + * + * @return an iterator over the list of heap ids + */ + public synchronized Iterator<Integer> getVmHeapIds() { + return mHeapInfoMap.keySet().iterator(); + } + + /** + * Returns the most-recent info values for the specified VM heap. + * + * @param heapId The heap whose info should be returned + * @return a map containing the info values for the specified heap. + * Returns <code>null</code> if the heap ID is unknown. + */ + public synchronized Map<String, Long> getVmHeapInfo(int heapId) { + return mHeapInfoMap.get(heapId); + } + + /** + * Adds a new thread to the list. + */ + synchronized void addThread(int threadId, String threadName) { + ThreadInfo attr = new ThreadInfo(threadId, threadName); + mThreadMap.put(threadId, attr); + } + + /** + * Removes a thread from the list. + */ + synchronized void removeThread(int threadId) { + mThreadMap.remove(threadId); + } + + /** + * Returns the list of threads as {@link ThreadInfo} objects. + * <p/>The list is empty until a thread update was requested with + * {@link Client#requestThreadUpdate()}. + */ + public synchronized ThreadInfo[] getThreads() { + Collection<ThreadInfo> threads = mThreadMap.values(); + return threads.toArray(new ThreadInfo[threads.size()]); + } + + /** + * Returns the {@link ThreadInfo} by thread id. + */ + synchronized ThreadInfo getThread(int threadId) { + return mThreadMap.get(threadId); + } + + synchronized void clearThreads() { + mThreadMap.clear(); + } + + /** + * Returns the list of {@link NativeAllocationInfo}. + * @see Client#requestNativeHeapInformation() + */ + public synchronized List<NativeAllocationInfo> getNativeAllocationList() { + return Collections.unmodifiableList(mNativeAllocationList); + } + + /** + * adds a new {@link NativeAllocationInfo} to the {@link Client} + * @param allocInfo The {@link NativeAllocationInfo} to add. + */ + synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) { + mNativeAllocationList.add(allocInfo); + } + + /** + * Clear the current malloc info. + */ + synchronized void clearNativeAllocationInfo() { + mNativeAllocationList.clear(); + } + + /** + * Returns the total native memory. + * @see Client#requestNativeHeapInformation() + */ + public synchronized int getTotalNativeMemory() { + return mNativeTotalMemory; + } + + synchronized void setTotalNativeMemory(int totalMemory) { + mNativeTotalMemory = totalMemory; + } + + synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) { + mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library)); + } + + /** + * Returns an {@link Iterator} on {@link NativeLibraryMapInfo} objects. + * <p/> + * The caller must synchronize on the {@link ClientData} object while iterating. + */ + public synchronized Iterator<NativeLibraryMapInfo> getNativeLibraryMapInfo() { + return mNativeLibMapInfo.iterator(); + } + + synchronized void setAllocationStatus(boolean enabled) { + mAllocationStatus = enabled ? ALLOCATION_TRACKING_ON : ALLOCATION_TRACKING_OFF; + } + + /** + * Returns the allocation tracking status. + * @see Client#requestAllocationStatus() + */ + public synchronized int getAllocationStatus() { + return mAllocationStatus; + } + + synchronized void setAllocations(AllocationInfo[] allocs) { + mAllocations = allocs; + } + + /** + * Returns the list of tracked allocations. + * @see Client#requestAllocationDetails() + */ + public synchronized AllocationInfo[] getAllocations() { + return mAllocations; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java new file mode 100644 index 0000000..c96d40d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +/** + * Preferences for the ddm library. + * <p/>This class does not handle storing the preferences. It is merely a central point for + * applications using the ddmlib to override the default values. + * <p/>Various components of the ddmlib query this class to get their values. + * <p/>Calls to some <code>set##()</code> methods will update the components using the values + * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)} + * has been called. + * <p/>Check the documentation of each method. + */ +public final class DdmPreferences { + + /** Default value for thread update flag upon client connection. */ + public final static boolean DEFAULT_INITIAL_THREAD_UPDATE = false; + /** Default value for heap update flag upon client connection. */ + public final static boolean DEFAULT_INITIAL_HEAP_UPDATE = false; + /** Default value for the selected client debug port */ + public final static int DEFAULT_SELECTED_DEBUG_PORT = 8700; + /** Default value for the debug port base */ + public final static int DEFAULT_DEBUG_PORT_BASE = 8600; + /** Default value for the logcat {@link LogLevel} */ + public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR; + + private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE; + private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE; + + private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT; + private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE; + private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL; + + /** + * Returns the initial {@link Client} flag for thread updates. + * @see #setInitialThreadUpdate(boolean) + */ + public static boolean getInitialThreadUpdate() { + return sThreadUpdate; + } + + /** + * Sets the initial {@link Client} flag for thread updates. + * <p/>This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialThreadUpdate(boolean state) { + sThreadUpdate = state; + } + + /** + * Returns the initial {@link Client} flag for heap updates. + * @see #setInitialHeapUpdate(boolean) + */ + public static boolean getInitialHeapUpdate() { + return sInitialHeapUpdate; + } + + /** + * Sets the initial {@link Client} flag for heap updates. + * <p/>If <code>true</code>, the {@link ClientData} will automatically be updated with + * the VM heap information whenever a GC happens. + * <p/>This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialHeapUpdate(boolean state) { + sInitialHeapUpdate = state; + } + + /** + * Returns the debug port used by the selected {@link Client}. + */ + public static int getSelectedDebugPort() { + return sSelectedDebugPort; + } + + /** + * Sets the debug port used by the selected {@link Client}. + * <p/>This change takes effect right away. + * @param port the new port to use. + */ + public static void setSelectedDebugPort(int port) { + sSelectedDebugPort = port; + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setDebugSelectedPort(port); + } + } + + /** + * Returns the debug port used by the first {@link Client}. Following clients, will use the + * next port. + */ + public static int getDebugPortBase() { + return sDebugPortBase; + } + + /** + * Sets the debug port used by the first {@link Client}. + * <p/>Once a port is used, the next Client will use port + 1. Quitting applications will + * release their debug port, and new clients will be able to reuse them. + * <p/>This must be called before {@link AndroidDebugBridge#init(boolean)}. + */ + public static void setDebugPortBase(int port) { + sDebugPortBase = port; + } + + /** + * Returns the minimum {@link LogLevel} being displayed. + */ + public static LogLevel getLogLevel() { + return sLogLevel; + } + + /** + * Sets the minimum {@link LogLevel} to display. + * <p/>This change takes effect right away. + */ + public static void setLogLevel(String value) { + sLogLevel = LogLevel.getByString(value); + + Log.setLevel(sLogLevel); + } + + /** + * Non accessible constructor. + */ + private DdmPreferences() { + // pass, only static methods in the class. + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java new file mode 100644 index 0000000..9392127 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Device; + +/** + * Centralized point to provide a {@link IDebugPortProvider} to ddmlib. + * + * <p/>When {@link Client} objects are created, they start listening for debuggers on a specific + * port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and + * increment this value for each new <code>Client</code>. + * + * <p/>This {@link DebugPortManager} allows applications using ddmlib to provide a custom + * port provider on a per-<code>Client</code> basis, depending on the device/emulator they are + * running on, and/or their names. + */ +public class DebugPortManager { + + /** + * Classes which implement this interface provide a method that provides a non random + * debugger port for a newly created {@link Client}. + */ + public interface IDebugPortProvider { + + public static final int NO_STATIC_PORT = -1; + + /** + * Returns a non-random debugger port for the specified application running on the + * specified {@link Device}. + * @param device The device the application is running on. + * @param appName The application name, as defined in the <code>AndroidManifest.xml</code> + * <var>package</var> attribute of the <var>manifest</var> node. + * @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client} + * should use the automatic debugger port provider. + */ + public int getPort(Device device, String appName); + } + + private static IDebugPortProvider sProvider = null; + + /** + * Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests + * a debugger port. + * @param provider the <code>IDebugPortProvider</code> to use. + */ + public static void setProvider(IDebugPortProvider provider) { + sProvider = provider; + } + + /** + * Returns the + * @return + */ + static IDebugPortProvider getProvider() { + return sProvider; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java new file mode 100644 index 0000000..f30509a --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +/** + * This represents a pending or established connection with a JDWP debugger. + */ +class Debugger { + + /* + * Messages from the debugger should be pretty small; may not even + * need an expanding-buffer implementation for this. + */ + private static final int INITIAL_BUF_SIZE = 1 * 1024; + private static final int MAX_BUF_SIZE = 32 * 1024; + private ByteBuffer mReadBuffer; + + private static final int PRE_DATA_BUF_SIZE = 256; + private ByteBuffer mPreDataBuffer; + + /* connection state */ + private int mConnState; + private static final int ST_NOT_CONNECTED = 1; + private static final int ST_AWAIT_SHAKE = 2; + private static final int ST_READY = 3; + + /* peer */ + private Client mClient; // client we're forwarding to/from + private int mListenPort; // listen to me + private ServerSocketChannel mListenChannel; + + /* this goes up and down; synchronize methods that access the field */ + private SocketChannel mChannel; + + /** + * Create a new Debugger object, configured to listen for connections + * on a specific port. + */ + Debugger(Client client, int listenPort) throws IOException { + + mClient = client; + mListenPort = listenPort; + + mListenChannel = ServerSocketChannel.open(); + mListenChannel.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), // $NON-NLS-1$ + listenPort); + mListenChannel.socket().setReuseAddress(true); // enable SO_REUSEADDR + mListenChannel.socket().bind(addr); + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE); + mConnState = ST_NOT_CONNECTED; + + Log.i("ddms", "Created: " + this.toString()); + } + + /** + * Returns "true" if a debugger is currently attached to us. + */ + boolean isDebuggerAttached() { + return mChannel != null; + } + + /** + * Represent the Debugger as a string. + */ + @Override + public String toString() { + // mChannel != null means we have connection, ST_READY means it's going + return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid() + + ((mConnState != ST_READY) ? " inactive]" : " active]"); + } + + /** + * Register the debugger's listen socket with the Selector. + */ + void registerListener(Selector sel) throws IOException { + mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this); + } + + /** + * Return the Client being debugged. + */ + Client getClient() { + return mClient; + } + + /** + * Accept a new connection, but only if we don't already have one. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept() throws IOException { + return accept(mListenChannel); + } + + /** + * Accept a new connection from the specified listen channel. This + * is so we can listen on a dedicated port for the "current" client, + * where "current" is constantly in flux. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept(ServerSocketChannel listenChan) + throws IOException { + + if (listenChan != null) { + SocketChannel newChan; + + newChan = listenChan.accept(); + if (mChannel != null) { + Log.w("ddms", "debugger already talking to " + mClient + + " on " + mListenPort); + newChan.close(); + return null; + } + mChannel = newChan; + mChannel.configureBlocking(false); // required for Selector + mConnState = ST_AWAIT_SHAKE; + return mChannel; + } + + return null; + } + + /** + * Close the data connection only. + */ + synchronized void closeData() { + try { + if (mChannel != null) { + mChannel.close(); + mChannel = null; + mConnState = ST_NOT_CONNECTED; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_DEFAULT); + mClient.update(Client.CHANGE_DEBUGGER_INTEREST); + } + } catch (IOException ioe) { + Log.w("ddms", "Failed to close data " + this); + } + } + + /** + * Close the socket that's listening for new connections and (if + * we're connected) the debugger data socket. + */ + synchronized void close() { + try { + if (mListenChannel != null) { + mListenChannel.close(); + } + mListenChannel = null; + closeData(); + } catch (IOException ioe) { + Log.w("ddms", "Failed to close listener " + this); + } + } + + // TODO: ?? add a finalizer that verifies the channel was closed + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() throws IOException { + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = + ByteBuffer.allocate(mReadBuffer.capacity() * 2); + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end + + mReadBuffer = newBuffer; + } + + count = mChannel.read(mReadBuffer); + Log.v("ddms", "Read " + count + " bytes from " + this); + if (count < 0) throw new IOException("read failed"); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. We also send + * the handshake response to the debugger, along with any pending + * pre-connection data, which is why this can throw an IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.i("ddms", "Good handshake from debugger"); + JdwpPacket.consumeHandshake(mReadBuffer); + sendHandshake(); + mConnState = ST_READY; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_ATTACHED); + mClient.update(Client.CHANGE_DEBUGGER_INTEREST); + + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + // not a debugger, throw an exception so we drop the line + Log.i("ddms", "Bad handshake from debugger"); + throw new IOException("bad handshake"); + case JdwpPacket.HANDSHAKE_NOTYET: + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_READY) { + if (mReadBuffer.position() != 0) { + Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /** + * Forward a packet to the client. + * + * "mClient" will never be null, though it's possible that the channel + * in the client has closed and our send attempt will fail. + * + * Consumes the packet. + */ + void forwardPacketToClient(JdwpPacket packet) throws IOException { + mClient.sendAndConsume(packet); + } + + /** + * Send the handshake to the debugger. We also send along any packets + * we already received from the client (usually just a VM_START event, + * if anything at all). + */ + private synchronized void sendHandshake() throws IOException { + ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN); + JdwpPacket.putHandshake(tempBuffer); + int expectedLength = tempBuffer.position(); + tempBuffer.flip(); + if (mChannel.write(tempBuffer) != expectedLength) { + throw new IOException("partial handshake write"); + } + + expectedLength = mPreDataBuffer.position(); + if (expectedLength > 0) { + Log.d("ddms", "Sending " + mPreDataBuffer.position() + + " bytes of saved data"); + mPreDataBuffer.flip(); + if (mChannel.write(mPreDataBuffer) != expectedLength) { + throw new IOException("partial pre-data write"); + } + mPreDataBuffer.clear(); + } + } + + /** + * Send a packet to the debugger. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + * + * We must synchronize on "mChannel" before writing to it. We want to + * coordinate the buffered data with mChannel creation, so this whole + * method is synchronized. + */ + synchronized void sendAndConsume(JdwpPacket packet) + throws IOException { + + if (mChannel == null) { + /* + * Buffer this up so we can send it to the debugger when it + * finally does connect. This is essential because the VM_START + * message might be telling the debugger that the VM is + * suspended. The alternative approach would be for us to + * capture and interpret VM_START and send it later if we + * didn't choose to un-suspend the VM for our own purposes. + */ + Log.d("ddms", "Saving packet 0x" + + Integer.toHexString(packet.getId())); + packet.movePacket(mPreDataBuffer); + } else { + packet.writeAndConsume(mChannel); + } + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java new file mode 100644 index 0000000..0e7f0bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * A Device. It can be a physical device or an emulator. + * + * TODO: make this class package-protected, and shift all callers to use IDevice + */ +public final class Device implements IDevice { + /** + * The state of a device. + */ + public static enum DeviceState { + BOOTLOADER("bootloader"), //$NON-NLS-1$ + OFFLINE("offline"), //$NON-NLS-1$ + ONLINE("device"); //$NON-NLS-1$ + + private String mState; + + DeviceState(String state) { + mState = state; + } + + /** + * Returns a {@link DeviceState} from the string returned by <code>adb devices</code>. + * @param state the device state. + * @return a {@link DeviceState} object or <code>null</code> if the state is unknown. + */ + public static DeviceState getState(String state) { + for (DeviceState deviceState : values()) { + if (deviceState.mState.equals(state)) { + return deviceState; + } + } + return null; + } + } + + /** Emulator Serial Number regexp. */ + final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ + + /** Serial number of the device */ + String serialNumber = null; + + /** Name of the AVD */ + String mAvdName = null; + + /** State of the device. */ + DeviceState state = null; + + /** Device properties. */ + private final Map<String, String> mProperties = new HashMap<String, String>(); + + private final ArrayList<Client> mClients = new ArrayList<Client>(); + private DeviceMonitor mMonitor; + + /** + * Socket for the connection monitoring client connection/disconnection. + */ + private SocketChannel mSocketChannel; + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSerialNumber() + */ + public String getSerialNumber() { + return serialNumber; + } + + public String getAvdName() { + return mAvdName; + } + + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getState() + */ + public DeviceState getState() { + return state; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperties() + */ + public Map<String, String> getProperties() { + return Collections.unmodifiableMap(mProperties); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getPropertyCount() + */ + public int getPropertyCount() { + return mProperties.size(); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperty(java.lang.String) + */ + public String getProperty(String name) { + return mProperties.get(name); + } + + + @Override + public String toString() { + return serialNumber; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOnline() + */ + public boolean isOnline() { + return state == DeviceState.ONLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isEmulator() + */ + public boolean isEmulator() { + return serialNumber.matches(RE_EMULATOR_SN); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOffline() + */ + public boolean isOffline() { + return state == DeviceState.OFFLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isBootLoader() + */ + public boolean isBootLoader() { + return state == DeviceState.BOOTLOADER; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#hasClients() + */ + public boolean hasClients() { + return mClients.size() > 0; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClients() + */ + public Client[] getClients() { + synchronized (mClients) { + return mClients.toArray(new Client[mClients.size()]); + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClient(java.lang.String) + */ + public Client getClient(String applicationName) { + synchronized (mClients) { + for (Client c : mClients) { + if (applicationName.equals(c.getClientData().getClientDescription())) { + return c; + } + } + + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSyncService() + */ + public SyncService getSyncService() { + SyncService syncService = new SyncService(AndroidDebugBridge.sSocketAddr, this); + if (syncService.openSync()) { + return syncService; + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getFileListingService() + */ + public FileListingService getFileListingService() { + return new FileListingService(this); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getScreenshot() + */ + public RawImage getScreenshot() throws IOException { + return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver) + */ + public void executeShellCommand(String command, IShellOutputReceiver receiver) + throws IOException { + AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this, + receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver) + */ + public void runEventLogService(LogReceiver receiver) throws IOException { + AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver) + */ + public void runLogService(String logname, + LogReceiver receiver) throws IOException { + AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#createForward(int, int) + */ + public boolean createForward(int localPort, int remotePort) { + try { + return AdbHelper.createForward(AndroidDebugBridge.sSocketAddr, this, + localPort, remotePort); + } catch (IOException e) { + Log.e("adb-forward", e); //$NON-NLS-1$ + return false; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#removeForward(int, int) + */ + public boolean removeForward(int localPort, int remotePort) { + try { + return AdbHelper.removeForward(AndroidDebugBridge.sSocketAddr, this, + localPort, remotePort); + } catch (IOException e) { + Log.e("adb-remove-forward", e); //$NON-NLS-1$ + return false; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClientName(int) + */ + public String getClientName(int pid) { + synchronized (mClients) { + for (Client c : mClients) { + if (c.getClientData().getPid() == pid) { + return c.getClientData().getClientDescription(); + } + } + } + + return null; + } + + + Device(DeviceMonitor monitor) { + mMonitor = monitor; + } + + DeviceMonitor getMonitor() { + return mMonitor; + } + + void addClient(Client client) { + synchronized (mClients) { + mClients.add(client); + } + } + + List<Client> getClientList() { + return mClients; + } + + boolean hasClient(int pid) { + synchronized (mClients) { + for (Client client : mClients) { + if (client.getClientData().getPid() == pid) { + return true; + } + } + } + + return false; + } + + void clearClientList() { + synchronized (mClients) { + mClients.clear(); + } + } + + /** + * Sets the client monitoring socket. + * @param socketChannel the sockets + */ + void setClientMonitoringSocket(SocketChannel socketChannel) { + mSocketChannel = socketChannel; + } + + /** + * Returns the client monitoring socket. + */ + SocketChannel getClientMonitoringSocket() { + return mSocketChannel; + } + + /** + * Removes a {@link Client} from the list. + * @param client the client to remove. + * @param notify Whether or not to notify the listeners of a change. + */ + void removeClient(Client client, boolean notify) { + mMonitor.addPortToAvailableList(client.getDebuggerListenPort()); + synchronized (mClients) { + mClients.remove(client); + } + if (notify) { + mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST); + } + } + + void update(int changeMask) { + mMonitor.getServer().deviceChanged(this, changeMask); + } + + void update(Client client, int changeMask) { + mMonitor.getServer().clientChanged(client, changeMask); + } + + void addProperty(String label, String value) { + mProperties.put(label, value); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java new file mode 100644 index 0000000..f9d0fa0 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java @@ -0,0 +1,866 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.Device.DeviceState; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * A Device monitor. This connects to the Android Debug Bridge and get device and + * debuggable process information from it. + */ +final class DeviceMonitor { + private byte[] mLengthBuffer = new byte[4]; + private byte[] mLengthBuffer2 = new byte[4]; + + private boolean mQuit = false; + + private AndroidDebugBridge mServer; + + private SocketChannel mMainAdbConnection = null; + private boolean mMonitoring = false; + private int mConnectionAttempt = 0; + private int mRestartAttemptCount = 0; + private boolean mInitialDeviceListDone = false; + + private Selector mSelector; + + private final ArrayList<Device> mDevices = new ArrayList<Device>(); + + private final ArrayList<Integer> mDebuggerPorts = new ArrayList<Integer>(); + + private final HashMap<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>(); + + /** + * Creates a new {@link DeviceMonitor} object and links it to the running + * {@link AndroidDebugBridge} object. + * @param server the running {@link AndroidDebugBridge}. + */ + DeviceMonitor(AndroidDebugBridge server) { + mServer = server; + + mDebuggerPorts.add(DdmPreferences.getDebugPortBase()); + } + + /** + * Starts the monitoring. + */ + void start() { + new Thread("Device List Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceMonitorLoop(); + } + }.start(); + } + + /** + * Stops the monitoring. + */ + void stop() { + mQuit = true; + + // wakeup the main loop thread by closing the main connection to adb. + try { + if (mMainAdbConnection != null) { + mMainAdbConnection.close(); + } + } catch (IOException e1) { + } + + // wake up the secondary loop by closing the selector. + if (mSelector != null) { + mSelector.wakeup(); + } + } + + + + /** + * Returns if the monitor is currently connected to the debug bridge server. + * @return + */ + boolean isMonitoring() { + return mMonitoring; + } + + int getConnectionAttemptCount() { + return mConnectionAttempt; + } + + int getRestartAttemptCount() { + return mRestartAttemptCount; + } + + /** + * Returns the devices. + */ + Device[] getDevices() { + synchronized (mDevices) { + return mDevices.toArray(new Device[mDevices.size()]); + } + } + + boolean hasInitialDeviceList() { + return mInitialDeviceListDone; + } + + AndroidDebugBridge getServer() { + return mServer; + } + + void addClientToDropAndReopen(Client client, int port) { + synchronized (mClientsToReopen) { + Log.d("DeviceMonitor", + "Adding " + client + " to list of client to reopen (" + port +")."); + if (mClientsToReopen.get(client) == null) { + mClientsToReopen.put(client, port); + } + } + mSelector.wakeup(); + } + + /** + * Monitors the devices. This connects to the Debug Bridge + */ + private void deviceMonitorLoop() { + do { + try { + if (mMainAdbConnection == null) { + Log.d("DeviceMonitor", "Opening adb connection"); + mMainAdbConnection = openAdbConnection(); + if (mMainAdbConnection == null) { + mConnectionAttempt++; + Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt); + if (mConnectionAttempt > 10) { + if (mServer.startAdb() == false) { + mRestartAttemptCount++; + Log.e("DeviceMonitor", + "adb restart attempts: " + mRestartAttemptCount); + } else { + mRestartAttemptCount = 0; + } + } + waitABit(); + } else { + Log.d("DeviceMonitor", "Connected to adb for device monitoring"); + mConnectionAttempt = 0; + } + } + + if (mMainAdbConnection != null && mMonitoring == false) { + mMonitoring = sendDeviceListMonitoringRequest(); + } + + if (mMonitoring) { + // read the length of the incoming message + int length = readLength(mMainAdbConnection, mLengthBuffer); + + if (length >= 0) { + // read the incoming message + processIncomingDeviceData(length); + + // flag the fact that we have build the list at least once. + mInitialDeviceListDone = true; + } + } + } catch (AsynchronousCloseException ace) { + // this happens because of a call to Quit. We do nothing, and the loop will break. + } catch (IOException ioe) { + if (mQuit == false) { + Log.e("DeviceMonitor", "Adb connection Error:" + ioe.getMessage()); + mMonitoring = false; + if (mMainAdbConnection != null) { + try { + mMainAdbConnection.close(); + } catch (IOException ioe2) { + // we can safely ignore that one. + } + mMainAdbConnection = null; + } + } + } + } while (mQuit == false); + } + + /** + * Sleeps for a little bit. + */ + private void waitABit() { + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + } + } + + /** + * Attempts to connect to the debug bridge server. + * @return a connect socket if success, null otherwise + */ + private SocketChannel openAdbConnection() { + Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring..."); + + SocketChannel adbChannel = null; + try { + adbChannel = SocketChannel.open(AndroidDebugBridge.sSocketAddr); + adbChannel.socket().setTcpNoDelay(true); + } catch (IOException e) { + } + + return adbChannel; + } + + /** + * + * @return + * @throws IOException + */ + private boolean sendDeviceListMonitoringRequest() throws IOException { + byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$ + + if (AdbHelper.write(mMainAdbConnection, request) == false) { + Log.e("DeviceMonitor", "Sending Tracking request failed!"); + mMainAdbConnection.close(); + throw new IOException("Sending Tracking request failed!"); + } + + AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection, + false /* readDiagString */); + + if (resp.ioSuccess == false) { + Log.e("DeviceMonitor", "Failed to read the adb response!"); + mMainAdbConnection.close(); + throw new IOException("Failed to read the adb response!"); + } + + if (resp.okay == false) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } + + /** + * Processes an incoming device message from the socket + * @param socket + * @param length + * @throws IOException + */ + private void processIncomingDeviceData(int length) throws IOException { + ArrayList<Device> list = new ArrayList<Device>(); + + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(mMainAdbConnection, buffer); + + String[] devices = result.split("\n"); // $NON-NLS-1$ + + for (String d : devices) { + String[] param = d.split("\t"); // $NON-NLS-1$ + if (param.length == 2) { + // new adb uses only serial numbers to identify devices + Device device = new Device(this); + device.serialNumber = param[0]; + device.state = DeviceState.getState(param[1]); + + //add the device to the list + list.add(device); + } + } + } + + // now merge the new devices with the old ones. + updateDevices(list); + } + + /** + * Updates the device list with the new items received from the monitoring service. + */ + private void updateDevices(ArrayList<Device> newList) { + // because we are going to call mServer.deviceDisconnected which will acquire this lock + // we lock it first, so that the AndroidDebugBridge lock is always locked first. + synchronized (AndroidDebugBridge.getLock()) { + synchronized (mDevices) { + // For each device in the current list, we look for a matching the new list. + // * if we find it, we update the current object with whatever new information + // there is + // (mostly state change, if the device becomes ready, we query for build info). + // We also remove the device from the new list to mark it as "processed" + // * if we do not find it, we remove it from the current list. + // Once this is done, the new list contains device we aren't monitoring yet, so we + // add them to the list, and start monitoring them. + + for (int d = 0 ; d < mDevices.size() ;) { + Device device = mDevices.get(d); + + // look for a similar device in the new list. + int count = newList.size(); + boolean foundMatch = false; + for (int dd = 0 ; dd < count ; dd++) { + Device newDevice = newList.get(dd); + // see if it matches in id and serial number. + if (newDevice.serialNumber.equals(device.serialNumber)) { + foundMatch = true; + + // update the state if needed. + if (device.state != newDevice.state) { + device.state = newDevice.state; + device.update(Device.CHANGE_STATE); + + // if the device just got ready/online, we need to start + // monitoring it. + if (device.isOnline()) { + if (AndroidDebugBridge.getClientSupport() == true) { + if (startMonitoringDevice(device) == false) { + Log.e("DeviceMonitor", + "Failed to start monitoring " + + device.serialNumber); + } + } + + if (device.getPropertyCount() == 0) { + queryNewDeviceForInfo(device); + } + } + } + + // remove the new device from the list since it's been used + newList.remove(dd); + break; + } + } + + if (foundMatch == false) { + // the device is gone, we need to remove it, and keep current index + // to process the next one. + removeDevice(device); + mServer.deviceDisconnected(device); + } else { + // process the next one + d++; + } + } + + // at this point we should still have some new devices in newList, so we + // process them. + for (Device newDevice : newList) { + // add them to the list + mDevices.add(newDevice); + mServer.deviceConnected(newDevice); + + // start monitoring them. + if (AndroidDebugBridge.getClientSupport() == true) { + if (newDevice.isOnline()) { + startMonitoringDevice(newDevice); + } + } + + // look for their build info. + if (newDevice.isOnline()) { + queryNewDeviceForInfo(newDevice); + } + } + } + } + newList.clear(); + } + + private void removeDevice(Device device) { + device.clearClientList(); + mDevices.remove(device); + + SocketChannel channel = device.getClientMonitoringSocket(); + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + // doesn't really matter if the close fails. + } + } + } + + /** + * Queries a device for its build info. + * @param device the device to query. + */ + private void queryNewDeviceForInfo(Device device) { + // TODO: do this in a separate thread. + try { + // first get the list of properties. + device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND, + new GetPropReceiver(device)); + + // now get the emulator Virtual Device name (if applicable). + if (device.isEmulator()) { + EmulatorConsole console = EmulatorConsole.getConsole(device); + if (console != null) { + device.mAvdName = console.getAvdName(); + } + } + } catch (IOException e) { + // if we can't get the build info, it doesn't matter too much + } + } + + /** + * Starts a monitoring service for a device. + * @param device the device to monitor. + * @return true if success. + */ + private boolean startMonitoringDevice(Device device) { + SocketChannel socketChannel = openAdbConnection(); + + if (socketChannel != null) { + try { + boolean result = sendDeviceMonitoringRequest(socketChannel, device); + if (result) { + + if (mSelector == null) { + startDeviceMonitorThread(); + } + + device.setClientMonitoringSocket(socketChannel); + + synchronized (mDevices) { + // always wakeup before doing the register. The synchronized block + // ensure that the selector won't select() before the end of this block. + // @see deviceClientMonitorLoop + mSelector.wakeup(); + + socketChannel.configureBlocking(false); + socketChannel.register(mSelector, SelectionKey.OP_READ, device); + } + + return true; + } + } catch (IOException e) { + try { + // attempt to close the socket if needed. + socketChannel.close(); + } catch (IOException e1) { + // we can ignore that one. It may already have been closed. + } + Log.d("DeviceMonitor", + "Connection Failure when starting to monitor device '" + + device + "' : " + e.getMessage()); + } + } + + return false; + } + + private void startDeviceMonitorThread() throws IOException { + mSelector = Selector.open(); + new Thread("Device Client Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceClientMonitorLoop(); + } + }.start(); + } + + private void deviceClientMonitorLoop() { + do { + try { + // This synchronized block stops us from doing the select() if a new + // Device is being added. + // @see startMonitoringDevice() + synchronized (mDevices) { + } + + int count = mSelector.select(); + + if (mQuit) { + return; + } + + synchronized (mClientsToReopen) { + if (mClientsToReopen.size() > 0) { + Set<Client> clients = mClientsToReopen.keySet(); + MonitorThread monitorThread = MonitorThread.getInstance(); + + for (Client client : clients) { + Device device = client.getDevice(); + int pid = client.getClientData().getPid(); + + monitorThread.dropClient(client, false /* notify */); + + // This is kinda bad, but if we don't wait a bit, the client + // will never answer the second handshake! + waitABit(); + + int port = mClientsToReopen.get(client); + + if (port == IDebugPortProvider.NO_STATIC_PORT) { + port = getNextDebuggerPort(); + } + Log.d("DeviceMonitor", "Reopening " + client); + openClient(device, pid, port, monitorThread); + device.update(Device.CHANGE_CLIENT_LIST); + } + + mClientsToReopen.clear(); + } + } + + if (count == 0) { + continue; + } + + Set<SelectionKey> keys = mSelector.selectedKeys(); + Iterator<SelectionKey> iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + if (key.isValid() && key.isReadable()) { + Object attachment = key.attachment(); + + if (attachment instanceof Device) { + Device device = (Device)attachment; + + SocketChannel socket = device.getClientMonitoringSocket(); + + if (socket != null) { + try { + int length = readLength(socket, mLengthBuffer2); + + processIncomingJdwpData(device, socket, length); + } catch (IOException ioe) { + Log.d("DeviceMonitor", + "Error reading jdwp list: " + ioe.getMessage()); + socket.close(); + + // restart the monitoring of that device + synchronized (mDevices) { + if (mDevices.contains(device)) { + Log.d("DeviceMonitor", + "Restarting monitoring service for " + device); + startMonitoringDevice(device); + } + } + } + } + } + } + } + } catch (IOException e) { + if (mQuit == false) { + + } + } + + } while (mQuit == false); + } + + private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device) + throws IOException { + + AdbHelper.setDevice(socket, device); + + byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$ + + if (AdbHelper.write(socket, request) == false) { + Log.e("DeviceMonitor", "Sending jdwp tracking request failed!"); + socket.close(); + throw new IOException(); + } + + AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */); + + if (resp.ioSuccess == false) { + Log.e("DeviceMonitor", "Failed to read the adb response!"); + socket.close(); + throw new IOException(); + } + + if (resp.okay == false) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } + + private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length) + throws IOException { + if (length >= 0) { + // array for the current pids. + ArrayList<Integer> pidList = new ArrayList<Integer>(); + + // get the string data if there are any + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(monitorSocket, buffer); + + // split each line in its own list and create an array of integer pid + String[] pids = result.split("\n"); //$NON-NLS-1$ + + for (String pid : pids) { + try { + pidList.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // looks like this pid is not really a number. Lets ignore it. + continue; + } + } + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + + // Now we merge the current list with the old one. + // this is the same mechanism as the merging of the device list. + + // For each client in the current list, we look for a matching the pid in the new list. + // * if we find it, we do nothing, except removing the pid from its list, + // to mark it as "processed" + // * if we do not find any match, we remove the client from the current list. + // Once this is done, the new list contains pids for which we don't have clients yet, + // so we create clients for them, add them to the list, and start monitoring them. + + List<Client> clients = device.getClientList(); + + boolean changed = false; + + // because MonitorThread#dropClient acquires first the monitorThread lock and then the + // Device client list lock (when removing the Client from the list), we have to make + // sure we acquire the locks in the same order, since another thread (MonitorThread), + // could call dropClient itself. + synchronized (monitorThread) { + synchronized (clients) { + for (int c = 0 ; c < clients.size() ;) { + Client client = clients.get(c); + int pid = client.getClientData().getPid(); + + // look for a matching pid + Integer match = null; + for (Integer matchingPid : pidList) { + if (pid == matchingPid.intValue()) { + match = matchingPid; + break; + } + } + + if (match != null) { + pidList.remove(match); + c++; // move on to the next client. + } else { + // we need to drop the client. the client will remove itself from the + // list of its device which is 'clients', so there's no need to + // increment c. + // We ask the monitor thread to not send notification, as we'll do + // it once at the end. + monitorThread.dropClient(client, false /* notify */); + changed = true; + } + } + } + } + + // at this point whatever pid is left in the list needs to be converted into Clients. + for (int newPid : pidList) { + openClient(device, newPid, getNextDebuggerPort(), monitorThread); + changed = true; + } + + if (changed) { + mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST); + } + } + } + + /** + * Opens and creates a new client. + * @return + */ + private void openClient(Device device, int pid, int port, MonitorThread monitorThread) { + + SocketChannel clientSocket; + try { + clientSocket = AdbHelper.createPassThroughConnection( + AndroidDebugBridge.sSocketAddr, device, pid); + + // required for Selector + clientSocket.configureBlocking(false); + } catch (UnknownHostException uhe) { + Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid); + return; + } catch (IOException ioe) { + Log.w("DeviceMonitor", + "Failed to connect to client '" + pid + "': " + ioe.getMessage()); + return ; + } + + createClient(device, pid, clientSocket, port, monitorThread); + } + + /** + * Creates a client and register it to the monitor thread + * @param device + * @param pid + * @param socket + * @param debuggerPort the debugger port. + * @param monitorThread the {@link MonitorThread} object. + */ + private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort, + MonitorThread monitorThread) { + + /* + * Successfully connected to something. Create a Client object, add + * it to the list, and initiate the JDWP handshake. + */ + + Client client = new Client(device, socket, pid); + + if (client.sendHandshake()) { + try { + if (AndroidDebugBridge.getClientSupport()) { + client.listenForDebugger(debuggerPort); + } + client.requestAllocationStatus(); + } catch (IOException ioe) { + client.getClientData().setDebuggerConnectionStatus(ClientData.DEBUGGER_ERROR); + Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger"); + // oh well + } + } else { + Log.e("ddms", "Handshake with " + client + " failed!"); + /* + * The handshake send failed. We could remove it now, but if the + * failure is "permanent" we'll just keep banging on it and + * getting the same result. Keep it in the list with its "error" + * state so we don't try to reopen it. + */ + } + + if (client.isValid()) { + device.addClient(client); + monitorThread.addClient(client); + } else { + client = null; + } + } + + private int getNextDebuggerPort() { + // get the first port and remove it + synchronized (mDebuggerPorts) { + if (mDebuggerPorts.size() > 0) { + int port = mDebuggerPorts.get(0); + + // remove it. + mDebuggerPorts.remove(0); + + // if there's nothing left, add the next port to the list + if (mDebuggerPorts.size() == 0) { + mDebuggerPorts.add(port+1); + } + + return port; + } + } + + return -1; + } + + void addPortToAvailableList(int port) { + if (port > 0) { + synchronized (mDebuggerPorts) { + // because there could be case where clients are closed twice, we have to make + // sure the port number is not already in the list. + if (mDebuggerPorts.indexOf(port) == -1) { + // add the port to the list while keeping it sorted. It's not like there's + // going to be tons of objects so we do it linearly. + int count = mDebuggerPorts.size(); + for (int i = 0 ; i < count ; i++) { + if (port < mDebuggerPorts.get(i)) { + mDebuggerPorts.add(i, port); + break; + } + } + // TODO: check if we can compact the end of the list. + } + } + } + } + + /** + * Reads the length of the next message from a socket. + * @param socket The {@link SocketChannel} to read from. + * @return the length, or 0 (zero) if no data is available from the socket. + * @throws IOException if the connection failed. + */ + private int readLength(SocketChannel socket, byte[] buffer) throws IOException { + String msg = read(socket, buffer); + + if (msg != null) { + try { + return Integer.parseInt(msg, 16); + } catch (NumberFormatException nfe) { + // we'll throw an exception below. + } + } + + // we receive something we can't read. It's better to reset the connection at this point. + throw new IOException("Unable to read length"); + } + + /** + * Fills a buffer from a socket. + * @param socket + * @param buffer + * @return the content of the buffer as a string, or null if it failed to convert the buffer. + * @throws IOException + */ + private String read(SocketChannel socket, byte[] buffer) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length); + + while (buf.position() != buf.limit()) { + int count; + + count = socket.read(buf); + if (count < 0) { + throw new IOException("EOF"); + } + } + + try { + return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + // we'll return null below. + } + + return null; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java new file mode 100644 index 0000000..f3986ed --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java @@ -0,0 +1,751 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.InvalidParameterException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides control over emulated hardware of the Android emulator. + * <p/>This is basically a wrapper around the command line console normally used with telnet. + *<p/> + * Regarding line termination handling:<br> + * One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most + * implementations don't enforce it (the dos one does). In this particular case, this is mostly + * irrelevant since we don't use telnet in Java, but that means we want to make + * sure we use the same line termination than what the console expects. The console + * code removes <code>\r</code> and waits for <code>\n</code>. + * <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console. + * <p/> + * <b>This API will change in the near future.</b> + */ +public final class EmulatorConsole { + + private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + private final static int WAIT_TIME = 5; // spin-wait sleep, in ms + + private final static int STD_TIMEOUT = 5000; // standard delay, in ms + + private final static String HOST = "127.0.0.1"; //$NON-NLS-1$ + + private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$ + private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$ + private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GPS = + "geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$ + "%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$ + "1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$ + + private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$ + + /** + * Array of delay values: no delay, gprs, edge/egprs, umts/3d + */ + public final static int[] MIN_LATENCIES = new int[] { + 0, // No delay + 150, // gprs + 80, // edge/egprs + 35 // umts/3g + }; + + /** + * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa. + */ + public final int[] DOWNLOAD_SPEEDS = new int[] { + 0, // full speed + 14400, // gsm + 43200, // hscsd + 80000, // gprs + 236800, // edge/egprs + 1920000, // umts/3g + 14400000 // hsdpa + }; + + /** Arrays of valid network speeds */ + public final static String[] NETWORK_SPEEDS = new String[] { + "full", //$NON-NLS-1$ + "gsm", //$NON-NLS-1$ + "hscsd", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + "hsdpa", //$NON-NLS-1$ + }; + + /** Arrays of valid network latencies */ + public final static String[] NETWORK_LATENCIES = new String[] { + "none", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + }; + + /** Gsm Mode enum. */ + public static enum GsmMode { + UNKNOWN((String)null), + UNREGISTERED(new String[] { "unregistered", "off" }), + HOME(new String[] { "home", "on" }), + ROAMING("roaming"), + SEARCHING("searching"), + DENIED("denied"); + + private final String[] tags; + + GsmMode(String tag) { + if (tag != null) { + this.tags = new String[] { tag }; + } else { + this.tags = new String[0]; + } + } + + GsmMode(String[] tags) { + this.tags = tags; + } + + public static GsmMode getEnum(String tag) { + for (GsmMode mode : values()) { + for (String t : mode.tags) { + if (t.equals(tag)) { + return mode; + } + } + } + return UNKNOWN; + } + + /** + * Returns the first tag of the enum. + */ + public String getTag() { + if (tags.length > 0) { + return tags[0]; + } + return null; + } + } + + public final static String RESULT_OK = null; + + private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN); + private final static Pattern sVoiceStatusRegexp = Pattern.compile( + "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sDataStatusRegexp = Pattern.compile( + "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sDownloadSpeedRegexp = Pattern.compile( + "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sMinLatencyRegexp = Pattern.compile( + "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private final static HashMap<Integer, EmulatorConsole> sEmulators = + new HashMap<Integer, EmulatorConsole>(); + + /** Gsm Status class */ + public static class GsmStatus { + /** Voice status. */ + public GsmMode voice = GsmMode.UNKNOWN; + /** Data status. */ + public GsmMode data = GsmMode.UNKNOWN; + } + + /** Network Status class */ + public static class NetworkStatus { + /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */ + public int speed = -1; + /** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */ + public int latency = -1; + } + + private int mPort; + + private SocketChannel mSocketChannel; + + private byte[] mBuffer = new byte[1024]; + + /** + * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can + * be an already existing console, or a new one if it hadn't been created yet. + * @param d The device that the console links to. + * @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed. + */ + public static synchronized EmulatorConsole getConsole(Device d) { + // we need to make sure that the device is an emulator + Matcher m = sEmulatorRegexp.matcher(d.serialNumber); + if (m.matches()) { + // get the port number. This is the console port. + int port; + try { + port = Integer.parseInt(m.group(1)); + if (port <= 0) { + return null; + } + } catch (NumberFormatException e) { + // looks like we failed to get the port number. This is a bit strange since + // it's coming from a regexp that only accept digit, but we handle the case + // and return null. + return null; + } + + EmulatorConsole console = sEmulators.get(port); + + if (console != null) { + // if the console exist, we ping the emulator to check the connection. + if (console.ping() == false) { + RemoveConsole(console.mPort); + console = null; + } + } + + if (console == null) { + // no console object exists for this port so we create one, and start + // the connection. + console = new EmulatorConsole(port); + if (console.start()) { + sEmulators.put(port, console); + } else { + console = null; + } + } + + return console; + } + + return null; + } + + /** + * Removes the console object associated with a port from the map. + * @param port The port of the console to remove. + */ + private static synchronized void RemoveConsole(int port) { + sEmulators.remove(port); + } + + private EmulatorConsole(int port) { + super(); + mPort = port; + } + + /** + * Starts the connection of the console. + * @return true if success. + */ + private boolean start() { + + InetSocketAddress socketAddr; + try { + InetAddress hostAddr = InetAddress.getByName(HOST); + socketAddr = new InetSocketAddress(hostAddr, mPort); + } catch (UnknownHostException e) { + return false; + } + + try { + mSocketChannel = SocketChannel.open(socketAddr); + } catch (IOException e1) { + return false; + } + + // read some stuff from it + readLines(); + + return true; + } + + /** + * Ping the emulator to check if the connection is still alive. + * @return true if the connection is alive. + */ + private synchronized boolean ping() { + // it looks like we can send stuff, even when the emulator quit, but we can't read + // from the socket. So we check the return of readLines() + if (sendCommand(COMMAND_PING)) { + return readLines() != null; + } + + return false; + } + + /** + * Sends a KILL command to the emulator. + */ + public synchronized void kill() { + if (sendCommand(COMMAND_KILL)) { + RemoveConsole(mPort); + } + } + + public synchronized String getAvdName() { + if (sendCommand(COMMAND_AVD_NAME)) { + String[] result = readLines(); + if (result != null && result.length == 2) { // this should be the name on first line, + // and ok on 2nd line + return result[0]; + } else { + // try to see if there's a message after KO + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + } + } + + return null; + } + + /** + * Get the network status of the emulator. + * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or + * <code>null</code> if the query failed. + */ + public synchronized NetworkStatus getNetworkStatus() { + if (sendCommand(COMMAND_NETWORK_STATUS)) { + /* Result is in the format + Current network status: + download speed: 14400 bits/s (1.8 KB/s) + upload speed: 14400 bits/s (1.8 KB/s) + minimum latency: 0 ms + maximum latency: 0 ms + */ + String[] result = readLines(); + + if (isValid(result)) { + // we only compare agains the min latency and the download speed + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + NetworkStatus status = new NetworkStatus(); + for (String line : result) { + Matcher m = sDownloadSpeedRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.speed = getSpeedIndex(value); + + // move on to next line. + continue; + } + + m = sMinLatencyRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.latency = getLatencyIndex(value); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Returns the current gsm status of the emulator + * @return a {@link GsmStatus} object containing the gms status, or <code>null</code> + * if the query failed. + */ + public synchronized GsmStatus getGsmStatus() { + if (sendCommand(COMMAND_GSM_STATUS)) { + /* + * result is in the format: + * gsm status + * gsm voice state: home + * gsm data state: home + */ + + String[] result = readLines(); + if (isValid(result)) { + + GsmStatus status = new GsmStatus(); + + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + for (String line : result) { + Matcher m = sVoiceStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.voice = GsmMode.getEnum(value.toLowerCase()); + + // move on to next line. + continue; + } + + m = sDataStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.data = GsmMode.getEnum(value.toLowerCase()); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Sets the GSM voice mode. + * @param mode the {@link GsmMode} value. + * @return RESULT_OK if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_VOICE, mode.getTag()); + return processCommand(command); + } + + /** + * Sets the GSM data mode. + * @param mode the {@link GsmMode} value + * @return {@link #RESULT_OK} if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_DATA, mode.getTag()); + return processCommand(command); + } + + /** + * Initiate an incoming call on the emulator. + * @param number a string representing the calling number. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String call(String number) { + String command = String.format(COMMAND_GSM_CALL, number); + return processCommand(command); + } + + /** + * Cancels a current call. + * @param number the number of the call to cancel + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String cancelCall(String number) { + String command = String.format(COMMAND_GSM_CANCEL_CALL, number); + return processCommand(command); + } + + /** + * Sends an SMS to the emulator + * @param number The sender phone number + * @param message The SMS message. \ characters must be escaped. The carriage return is + * the 2 character sequence {'\', 'n' } + * + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String sendSms(String number, String message) { + String command = String.format(COMMAND_SMS_SEND, number, message); + return processCommand(command); + } + + /** + * Sets the network speed. + * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkSpeed(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]); + return processCommand(command); + } + + /** + * Sets the network latency. + * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkLatency(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]); + return processCommand(command); + } + + public synchronized String sendLocation(double longitude, double latitude, double elevation) { + + Calendar c = Calendar.getInstance(); + + double absLong = Math.abs(longitude); + int longDegree = (int)Math.floor(absLong); + char longDirection = 'E'; + if (longitude < 0) { + longDirection = 'W'; + } + + double longMinute = (absLong - Math.floor(absLong)) * 60; + + double absLat = Math.abs(latitude); + int latDegree = (int)Math.floor(absLat); + char latDirection = 'N'; + if (latitude < 0) { + latDirection = 'S'; + } + + double latMinute = (absLat - Math.floor(absLat)) * 60; + + String command = String.format(COMMAND_GPS, + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), + c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND), + latDegree, latMinute, latDirection, + longDegree, longMinute, longDirection); + + return processCommand(command); + } + + /** + * Sends a command to the emulator console. + * @param command The command string. <b>MUST BE TERMINATED BY \n</b>. + * @return true if success + */ + private boolean sendCommand(String command) { + boolean result = false; + try { + byte[] bCommand; + try { + bCommand = command.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + // wrong encoding... + return result; + } + + // write the command + AdbHelper.write(mSocketChannel, bCommand, bCommand.length, AdbHelper.STD_TIMEOUT); + + result = true; + } catch (IOException e) { + return false; + } finally { + if (result == false) { + // FIXME connection failed somehow, we need to disconnect the console. + RemoveConsole(mPort); + } + } + + return result; + } + + /** + * Sends a command to the emulator and parses its answer. + * @param command the command to send. + * @return {@link #RESULT_OK} if success, an error message otherwise. + */ + private String processCommand(String command) { + if (sendCommand(command)) { + String[] result = readLines(); + + if (result != null && result.length > 0) { + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + return RESULT_OK; + } + + return "Unable to communicate with the emulator"; + } + + return "Unable to send command to the emulator"; + } + + /** + * Reads line from the console socket. This call is blocking until we read the lines: + * <ul> + * <li>OK\r\n</li> + * <li>KO<msg>\r\n</li> + * </ul> + * @return the array of strings read from the emulator. + */ + private String[] readLines() { + try { + ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length); + int numWaits = 0; + boolean stop = false; + + while (buf.position() != buf.limit() && stop == false) { + int count; + + count = mSocketChannel.read(buf); + if (count < 0) { + return null; + } else if (count == 0) { + if (numWaits * WAIT_TIME > STD_TIMEOUT) { + return null; + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + + // check the last few char aren't OK. For a valid message to test + // we need at least 4 bytes (OK/KO + \r\n) + if (buf.position() >= 4) { + int pos = buf.position(); + if (endsWithOK(pos) || lastLineIsKO(pos)) { + stop = true; + } + } + } + + String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING); + return msg.split("\r\n"); //$NON-NLS-1$ + } catch (IOException e) { + return null; + } + } + + /** + * Returns true if the 4 characters *before* the current position are "OK\r\n" + * @param currentPosition The current position + */ + private boolean endsWithOK(int currentPosition) { + if (mBuffer[currentPosition-1] == '\n' && + mBuffer[currentPosition-2] == '\r' && + mBuffer[currentPosition-3] == 'K' && + mBuffer[currentPosition-4] == 'O') { + return true; + } + + return false; + } + + /** + * Returns true if the last line starts with KO and is also terminated by \r\n + * @param currentPosition the current position + */ + private boolean lastLineIsKO(int currentPosition) { + // first check that the last 2 characters are CRLF + if (mBuffer[currentPosition-1] != '\n' || + mBuffer[currentPosition-2] != '\r') { + return false; + } + + // now loop backward looking for the previous CRLF, or the beginning of the buffer + int i = 0; + for (i = currentPosition-3 ; i >= 0; i--) { + if (mBuffer[i] == '\n') { + // found \n! + if (i > 0 && mBuffer[i-1] == '\r') { + // found \r! + break; + } + } + } + + // here it is either -1 if we reached the start of the buffer without finding + // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2 + if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') { + // found error! + return true; + } + + return false; + } + + /** + * Returns true if the last line of the result does not start with KO + */ + private boolean isValid(String[] result) { + if (result != null && result.length > 0) { + return !(RE_KO.matcher(result[result.length-1]).matches()); + } + return false; + } + + private int getLatencyIndex(String value) { + try { + // get the int value + int latency = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < MIN_LATENCIES.length; i++) { + if (MIN_LATENCIES[i] == latency) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } + + private int getSpeedIndex(String value) { + try { + // get the int value + int speed = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) { + if (DOWNLOAD_SPEEDS[i] == speed) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java new file mode 100644 index 0000000..b50cf79 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java @@ -0,0 +1,767 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides {@link Device} side file listing service. + * <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}. + */ +public final class FileListingService { + + /** Pattern to find filenames that match "*.apk" */ + private final static Pattern sApkPattern = + Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$ + + /** Pattern to parse the output of the 'pm -lf' command.<br> + * The output format looks like:<br> + * /data/app/myapp.apk=com.mypackage.myapp */ + private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$ + + /** Top level data folder. */ + public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$ + /** Top level sdcard folder. */ + public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$ + /** Top level system folder. */ + public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$ + /** Top level temp folder. */ + public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$ + /** Application folder. */ + public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$ + + private final static String[] sRootLevelApprovedItems = { + DIRECTORY_DATA, + DIRECTORY_SDCARD, + DIRECTORY_SYSTEM, + DIRECTORY_TEMP + }; + + public static final long REFRESH_RATE = 5000L; + /** + * Refresh test has to be slightly lower for precision issue. + */ + static final long REFRESH_TEST = (long)(REFRESH_RATE * .8); + + /** Entry type: File */ + public static final int TYPE_FILE = 0; + /** Entry type: Directory */ + public static final int TYPE_DIRECTORY = 1; + /** Entry type: Directory Link */ + public static final int TYPE_DIRECTORY_LINK = 2; + /** Entry type: Block */ + public static final int TYPE_BLOCK = 3; + /** Entry type: Character */ + public static final int TYPE_CHARACTER = 4; + /** Entry type: Link */ + public static final int TYPE_LINK = 5; + /** Entry type: Socket */ + public static final int TYPE_SOCKET = 6; + /** Entry type: FIFO */ + public static final int TYPE_FIFO = 7; + /** Entry type: Other */ + public static final int TYPE_OTHER = 8; + + /** Device side file separator. */ + public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$ + + private static final String FILE_ROOT = "/"; //$NON-NLS-1$ + + + /** + * Regexp pattern to parse the result from ls. + */ + private static Pattern sLsPattern = Pattern.compile( + "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$ + + private Device mDevice; + private FileEntry mRoot; + + private ArrayList<Thread> mThreadList = new ArrayList<Thread>(); + + /** + * Represents an entry in a directory. This can be a file or a directory. + */ + public final static class FileEntry { + /** Pattern to escape filenames for shell command consumption. */ + private final static Pattern sEscapePattern = Pattern.compile( + "([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$ + + /** + * Comparator object for FileEntry + */ + private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() { + public int compare(FileEntry o1, FileEntry o2) { + if (o1 instanceof FileEntry && o2 instanceof FileEntry) { + FileEntry fe1 = (FileEntry)o1; + FileEntry fe2 = (FileEntry)o2; + return fe1.name.compareTo(fe2.name); + } + return 0; + } + }; + + FileEntry parent; + String name; + String info; + String permissions; + String size; + String date; + String time; + String owner; + String group; + int type; + boolean isAppPackage; + + boolean isRoot; + + /** + * Indicates whether the entry content has been fetched yet, or not. + */ + long fetchTime = 0; + + final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>(); + + /** + * Creates a new file entry. + * @param parent parent entry or null if entry is root + * @param name name of the entry. + * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE}, + * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}. + */ + private FileEntry(FileEntry parent, String name, int type, boolean isRoot) { + this.parent = parent; + this.name = name; + this.type = type; + this.isRoot = isRoot; + + checkAppPackageStatus(); + } + + /** + * Returns the name of the entry + */ + public String getName() { + return name; + } + + /** + * Returns the size string of the entry, as returned by <code>ls</code>. + */ + public String getSize() { + return size; + } + + /** + * Returns the size of the entry. + */ + public int getSizeValue() { + return Integer.parseInt(size); + } + + /** + * Returns the date string of the entry, as returned by <code>ls</code>. + */ + public String getDate() { + return date; + } + + /** + * Returns the time string of the entry, as returned by <code>ls</code>. + */ + public String getTime() { + return time; + } + + /** + * Returns the permission string of the entry, as returned by <code>ls</code>. + */ + public String getPermissions() { + return permissions; + } + + /** + * Returns the extra info for the entry. + * <p/>For a link, it will be a description of the link. + * <p/>For an application apk file it will be the application package as returned + * by the Package Manager. + */ + public String getInfo() { + return info; + } + + /** + * Return the full path of the entry. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator. + */ + public String getFullPath() { + if (isRoot) { + return FILE_ROOT; + } + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, false); + + return pathBuilder.toString(); + } + + /** + * Return the fully escaped path of the entry. This path is safe to use in a + * shell command line. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator + */ + public String getFullEscapedPath() { + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, true); + + return pathBuilder.toString(); + } + + /** + * Returns the path as a list of segments. + */ + public String[] getPathSegments() { + ArrayList<String> list = new ArrayList<String>(); + fillPathSegments(list); + + return list.toArray(new String[list.size()]); + } + + /** + * Returns true if the entry is a directory, false otherwise; + */ + public int getType() { + return type; + } + + /** + * Returns if the entry is a folder or a link to a folder. + */ + public boolean isDirectory() { + return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK; + } + + /** + * Returns the parent entry. + */ + public FileEntry getParent() { + return parent; + } + + /** + * Returns the cached children of the entry. This returns the cache created from calling + * <code>FileListingService.getChildren()</code>. + */ + public FileEntry[] getCachedChildren() { + return mChildren.toArray(new FileEntry[mChildren.size()]); + } + + /** + * Returns the child {@link FileEntry} matching the name. + * This uses the cached children list. + * @param name the name of the child to return. + * @return the FileEntry matching the name or null. + */ + public FileEntry findChild(String name) { + for (FileEntry entry : mChildren) { + if (entry.name.equals(name)) { + return entry; + } + } + return null; + } + + /** + * Returns whether the entry is the root. + */ + public boolean isRoot() { + return isRoot; + } + + void addChild(FileEntry child) { + mChildren.add(child); + } + + void setChildren(ArrayList<FileEntry> newChildren) { + mChildren.clear(); + mChildren.addAll(newChildren); + } + + boolean needFetch() { + if (fetchTime == 0) { + return true; + } + long current = System.currentTimeMillis(); + if (current-fetchTime > REFRESH_TEST) { + return true; + } + + return false; + } + + /** + * Returns if the entry is a valid application package. + */ + public boolean isApplicationPackage() { + return isAppPackage; + } + + /** + * Returns if the file name is an application package name. + */ + public boolean isAppFileName() { + Matcher m = sApkPattern.matcher(name); + return m.matches(); + } + + /** + * Recursively fills the pathBuilder with the full path + * @param pathBuilder a StringBuilder used to create the path. + * @param escapePath Whether the path need to be escaped for consumption by + * a shell command line. + */ + protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathBuilder(pathBuilder, escapePath); + } + pathBuilder.append(FILE_SEPARATOR); + pathBuilder.append(escapePath ? escape(name) : name); + } + + /** + * Recursively fills the segment list with the full path. + * @param list The list of segments to fill. + */ + protected void fillPathSegments(ArrayList<String> list) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathSegments(list); + } + + list.add(name); + } + + /** + * Sets the internal app package status flag. This checks whether the entry is in an app + * directory like /data/app or /system/app + */ + private void checkAppPackageStatus() { + isAppPackage = false; + + String[] segments = getPathSegments(); + if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) { + isAppPackage = DIRECTORY_APP.equals(segments[1]) && + (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0])); + } + } + + /** + * Returns an escaped version of the entry name. + * @param entryName + */ + private String escape(String entryName) { + return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$ + } + } + + private class LsReceiver extends MultiLineReceiver { + + private ArrayList<FileEntry> mEntryList; + private ArrayList<String> mLinkList; + private FileEntry[] mCurrentChildren; + private FileEntry mParentEntry; + + /** + * Create an ls receiver/parser. + * @param currentChildren The list of current children. To prevent + * collapse during update, reusing the same FileEntry objects for + * files that were already there is paramount. + * @param entryList the list of new children to be filled by the + * receiver. + * @param linkList the list of link path to compute post ls, to figure + * out if the link pointed to a file or to a directory. + */ + public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList, + ArrayList<String> linkList) { + mParentEntry = parentEntry; + mCurrentChildren = parentEntry.getCachedChildren(); + mEntryList = entryList; + mLinkList = linkList; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + // no need to handle empty lines. + if (line.length() == 0) { + continue; + } + + // run the line through the regexp + Matcher m = sLsPattern.matcher(line); + if (m.matches() == false) { + continue; + } + + // get the name + String name = m.group(7); + + // if the parent is root, we only accept selected items + if (mParentEntry.isRoot()) { + boolean found = false; + for (String approved : sRootLevelApprovedItems) { + if (approved.equals(name)) { + found = true; + break; + } + } + + // if it's not in the approved list we skip this entry. + if (found == false) { + continue; + } + } + + // get the rest of the groups + String permissions = m.group(1); + String owner = m.group(2); + String group = m.group(3); + String size = m.group(4); + String date = m.group(5); + String time = m.group(6); + String info = null; + + // and the type + int objectType = TYPE_OTHER; + switch (permissions.charAt(0)) { + case '-' : + objectType = TYPE_FILE; + break; + case 'b' : + objectType = TYPE_BLOCK; + break; + case 'c' : + objectType = TYPE_CHARACTER; + break; + case 'd' : + objectType = TYPE_DIRECTORY; + break; + case 'l' : + objectType = TYPE_LINK; + break; + case 's' : + objectType = TYPE_SOCKET; + break; + case 'p' : + objectType = TYPE_FIFO; + break; + } + + + // now check what we may be linking to + if (objectType == TYPE_LINK) { + String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$ + + // we should have 2 segments + if (segments.length == 2) { + // update the entry name to not contain the link + name = segments[0]; + + // and the link name + info = segments[1]; + + // now get the path to the link + String[] pathSegments = info.split(FILE_SEPARATOR); + if (pathSegments.length == 1) { + // the link is to something in the same directory, + // unless the link is .. + if ("..".equals(pathSegments[0])) { //$NON-NLS-1$ + // set the type and we're done. + objectType = TYPE_DIRECTORY_LINK; + } else { + // either we found the object already + // or we'll find it later. + } + } + } + + // add an arrow in front to specify it's a link. + info = "-> " + info; //$NON-NLS-1$; + } + + // get the entry, either from an existing one, or a new one + FileEntry entry = getExistingEntry(name); + if (entry == null) { + entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */); + } + + // add some misc info + entry.permissions = permissions; + entry.size = size; + entry.date = date; + entry.time = time; + entry.owner = owner; + entry.group = group; + if (objectType == TYPE_LINK) { + entry.info = info; + } + + mEntryList.add(entry); + } + } + + /** + * Queries for an already existing Entry per name + * @param name the name of the entry + * @return the existing FileEntry or null if no entry with a matching + * name exists. + */ + private FileEntry getExistingEntry(String name) { + for (int i = 0 ; i < mCurrentChildren.length; i++) { + FileEntry e = mCurrentChildren[i]; + + // since we're going to "erase" the one we use, we need to + // check that the item is not null. + if (e != null) { + // compare per name, case-sensitive. + if (name.equals(e.name)) { + // erase from the list + mCurrentChildren[i] = null; + + // and return the object + return e; + } + } + } + + // couldn't find any matching object, return null + return null; + } + + public boolean isCancelled() { + return false; + } + + public void finishLinks() { + // TODO Handle links in the listing service + } + } + + /** + * Classes which implement this interface provide a method that deals with asynchronous + * result from <code>ls</code> command on the device. + * + * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver) + */ + public interface IListingReceiver { + public void setChildren(FileEntry entry, FileEntry[] children); + + public void refreshEntry(FileEntry entry); + } + + /** + * Creates a File Listing Service for a specified {@link Device}. + * @param device The Device the service is connected to. + */ + FileListingService(Device device) { + mDevice = device; + } + + /** + * Returns the root element. + * @return the {@link FileEntry} object representing the root element or + * <code>null</code> if the device is invalid. + */ + public FileEntry getRoot() { + if (mDevice != null) { + if (mRoot == null) { + mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY, + true /* isRoot */); + } + + return mRoot; + } + + return null; + } + + /** + * Returns the children of a {@link FileEntry}. + * <p/> + * This method supports a cache mechanism and synchronous and asynchronous modes. + * <p/> + * If <var>receiver</var> is <code>null</code>, the device side <code>ls</code> + * command is done synchronously, and the method will return upon completion of the command.<br> + * If <var>receiver</var> is non <code>null</code>, the command is launched is a separate + * thread and upon completion, the receiver will be notified of the result. + * <p/> + * The result for each <code>ls</code> command is cached in the parent + * <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the + * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms. + * After that a new <code>ls</code> command is always executed. + * <p/> + * If the cache is valid and <code>useCache == true</code>, the method will always simply + * return the value of the cache, whether a {@link IListingReceiver} has been provided or not. + * + * @param entry The parent entry. + * @param useCache A flag to use the cache or to force a new ls command. + * @param receiver A receiver for asynchronous calls. + * @return The list of children or <code>null</code> for asynchronous calls. + * + * @see FileEntry#getCachedChildren() + */ + public FileEntry[] getChildren(final FileEntry entry, boolean useCache, + final IListingReceiver receiver) { + // first thing we do is check the cache, and if we already have a recent + // enough children list, we just return that. + if (useCache && entry.needFetch() == false) { + return entry.getCachedChildren(); + } + + // if there's no receiver, then this is a synchronous call, and we + // return the result of ls + if (receiver == null) { + doLs(entry); + return entry.getCachedChildren(); + } + + // this is a asynchronous call. + // we launch a thread that will do ls and give the listing + // to the receiver + Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$ + @Override + public void run() { + doLs(entry); + + receiver.setChildren(entry, entry.getCachedChildren()); + + final FileEntry[] children = entry.getCachedChildren(); + if (children.length > 0 && children[0].isApplicationPackage()) { + final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>(); + + for (FileEntry child : children) { + String path = child.getFullPath(); + map.put(path, child); + } + + // call pm. + String command = PM_FULL_LISTING; + try { + mDevice.executeShellCommand(command, new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.length() > 0) { + // get the filepath and package from the line + Matcher m = sPmPattern.matcher(line); + if (m.matches()) { + // get the children with that path + FileEntry entry = map.get(m.group(1)); + if (entry != null) { + entry.info = m.group(2); + receiver.refreshEntry(entry); + } + } + } + } + } + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. + } + } + + + // if another thread is pending, launch it + synchronized (mThreadList) { + // first remove ourselves from the list + mThreadList.remove(this); + + // then launch the next one if applicable. + if (mThreadList.size() > 0) { + Thread t = mThreadList.get(0); + t.start(); + } + } + } + }; + + // we don't want to run multiple ls on the device at the same time, so we + // store the thread in a list and launch it only if there's no other thread running. + // the thread will launch the next one once it's done. + synchronized (mThreadList) { + // add to the list + mThreadList.add(t); + + // if it's the only one, launch it. + if (mThreadList.size() == 1) { + t.start(); + } + } + + // and we return null. + return null; + } + + private void doLs(FileEntry entry) { + // create a list that will receive the list of the entries + ArrayList<FileEntry> entryList = new ArrayList<FileEntry>(); + + // create a list that will receive the link to compute post ls; + ArrayList<String> linkList = new ArrayList<String>(); + + try { + // create the command + String command = "ls -l " + entry.getFullPath(); //$NON-NLS-1$ + + // create the receiver object that will parse the result from ls + LsReceiver receiver = new LsReceiver(entry, entryList, linkList); + + // call ls. + mDevice.executeShellCommand(command, receiver); + + // finish the process of the receiver to handle links + receiver.finishLinks(); + } catch (IOException e) { + } + + + // at this point we need to refresh the viewer + entry.fetchTime = System.currentTimeMillis(); + + // sort the children and set them as the new children + Collections.sort(entryList, FileEntry.sEntryComparator); + entry.setChildren(entryList); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java new file mode 100644 index 0000000..9293379 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A receiver able to parse the result of the execution of + * {@link #GETPROP_COMMAND} on a device. + */ +final class GetPropReceiver extends MultiLineReceiver { + final static String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$ + + private final static Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$ + + /** indicates if we need to read the first */ + private Device mDevice = null; + + /** + * Creates the receiver with the device the receiver will modify. + * @param device The device to modify + */ + public GetPropReceiver(Device device) { + mDevice = device; + } + + @Override + public void processNewLines(String[] lines) { + // We receive an array of lines. We're expecting + // to have the build info in the first line, and the build + // date in the 2nd line. There seems to be an empty line + // after all that. + + for (String line : lines) { + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + Matcher m = GETPROP_PATTERN.matcher(line); + if (m.matches()) { + String label = m.group(1); + String value = m.group(2); + + if (label.length() > 0) { + mDevice.addProperty(label, value); + } + } + } + } + + public boolean isCancelled() { + return false; + } + + @Override + public void done() { + mDevice.update(Device.CHANGE_BUILD_INFO); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java new file mode 100644 index 0000000..99bd4d0 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "app name" chunk (APNM). + */ +final class HandleAppName extends ChunkHandler { + + public static final int CHUNK_APNM = ChunkHandler.type("APNM"); + + private static final HandleAppName mInst = new HandleAppName(); + + + private HandleAppName() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_APNM, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + + Log.d("ddm-appname", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_APNM) { + assert !isReply; + handleAPNM(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our APNM message. + */ + private static void handleAPNM(Client client, ByteBuffer data) { + int appNameLen; + String appName; + + appNameLen = data.getInt(); + appName = getString(data, appNameLen); + + Log.i("ddm-appname", "APNM: app='" + appName + "'"); + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setClientDescription(appName); + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + } + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java new file mode 100644 index 0000000..adeedbb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Submit an exit request. + */ +final class HandleExit extends ChunkHandler { + + public static final int CHUNK_EXIT = type("EXIT"); + + private static final HandleExit mInst = new HandleExit(); + + + private HandleExit() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) {} + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + /** + * Send an EXIT request to the client. + */ + public static void sendEXIT(Client client, int status) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(status); + + finishChunkPacket(packet, CHUNK_EXIT, buf.position()); + Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java new file mode 100644 index 0000000..5752b86 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Handle heap status updates. + */ +final class HandleHeap extends ChunkHandler { + + public static final int CHUNK_HPIF = type("HPIF"); + public static final int CHUNK_HPST = type("HPST"); + public static final int CHUNK_HPEN = type("HPEN"); + public static final int CHUNK_HPSG = type("HPSG"); + public static final int CHUNK_HPGC = type("HPGC"); + public static final int CHUNK_REAE = type("REAE"); + public static final int CHUNK_REAQ = type("REAQ"); + public static final int CHUNK_REAL = type("REAL"); + + // args to sendHPSG + public static final int WHEN_DISABLE = 0; + public static final int WHEN_GC = 1; + public static final int WHAT_MERGE = 0; // merge adjacent objects + public static final int WHAT_OBJ = 1; // keep objects distinct + + // args to sendHPIF + public static final int HPIF_WHEN_NEVER = 0; + public static final int HPIF_WHEN_NOW = 1; + public static final int HPIF_WHEN_NEXT_GC = 2; + public static final int HPIF_WHEN_EVERY_GC = 3; + + private static final HandleHeap mInst = new HandleHeap(); + + private HandleHeap() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HPIF, mInst); + mt.registerChunkHandler(CHUNK_HPST, mInst); + mt.registerChunkHandler(CHUNK_HPEN, mInst); + mt.registerChunkHandler(CHUNK_HPSG, mInst); + mt.registerChunkHandler(CHUNK_REAQ, mInst); + mt.registerChunkHandler(CHUNK_REAL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + if (client.isHeapUpdateEnabled()) { + //sendHPSG(client, WHEN_GC, WHAT_MERGE); + sendHPIF(client, HPIF_WHEN_EVERY_GC); + } + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + Log.d("ddm-heap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HPIF) { + handleHPIF(client, data); + client.update(Client.CHANGE_HEAP_DATA); + } else if (type == CHUNK_HPST) { + handleHPST(client, data); + } else if (type == CHUNK_HPEN) { + handleHPEN(client, data); + client.update(Client.CHANGE_HEAP_DATA); + } else if (type == CHUNK_HPSG) { + handleHPSG(client, data); + } else if (type == CHUNK_REAQ) { + handleREAQ(client, data); + client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS); + } else if (type == CHUNK_REAL) { + handleREAL(client, data); + client.update(Client.CHANGE_HEAP_ALLOCATIONS); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a heap info message. + */ + private void handleHPIF(Client client, ByteBuffer data) { + Log.d("ddm-heap", "HPIF!"); + try { + int numHeaps = data.getInt(); + + for (int i = 0; i < numHeaps; i++) { + int heapId = data.getInt(); + @SuppressWarnings("unused") + long timeStamp = data.getLong(); + @SuppressWarnings("unused") + byte reason = data.get(); + long maxHeapSize = (long)data.getInt() & 0x00ffffffff; + long heapSize = (long)data.getInt() & 0x00ffffffff; + long bytesAllocated = (long)data.getInt() & 0x00ffffffff; + long objectsAllocated = (long)data.getInt() & 0x00ffffffff; + + client.getClientData().setHeapInfo(heapId, maxHeapSize, + heapSize, bytesAllocated, objectsAllocated); + } + } catch (BufferUnderflowException ex) { + Log.w("ddm-heap", "malformed HPIF chunk from client"); + } + } + + /** + * Send an HPIF (HeaP InFo) request to the client. + */ + public static void sendHPIF(Client client, int when) throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + + finishChunkPacket(packet, CHUNK_HPIF, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle a heap segment series start message. + */ + private void handleHPST(Client client, ByteBuffer data) { + /* Clear out any data that's sitting around to + * get ready for the chunks that are about to come. + */ +//xxx todo: only clear data that belongs to the heap mentioned in <data>. + client.getClientData().getVmHeapData().clearHeapData(); + } + + /* + * Handle a heap segment series end message. + */ + private void handleHPEN(Client client, ByteBuffer data) { + /* Let the UI know that we've received all of the + * data for this heap. + */ +//xxx todo: only seal data that belongs to the heap mentioned in <data>. + client.getClientData().getVmHeapData().sealHeapData(); + } + + /* + * Handle a heap segment message. + */ + private void handleHPSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getVmHeapData().addHeapData(data); +//xxx todo: add to the heap mentioned in <data> + } + + /** + * Sends an HPSG (HeaP SeGment) request to the client. + */ + public static void sendHPSG(Client client, int when, int what) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + buf.put((byte)what); + + finishChunkPacket(packet, CHUNK_HPSG, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when=" + + when + ", what=" + what); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends an HPGC request to the client. + */ + public static void sendHPGC(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_HPGC, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAE (REcent Allocation Enable) request to the client. + */ + public static void sendREAE(Client client, boolean enable) + throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte) (enable ? 1 : 0)); + + finishChunkPacket(packet, CHUNK_REAE, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAQ (REcent Allocation Query) request to the client. + */ + public static void sendREAQ(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAQ, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAL (REcent ALlocation) request to the client. + */ + public static void sendREAL(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAL, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAL)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle the response from our REcent Allocation Query message. + */ + private void handleREAQ(Client client, ByteBuffer data) { + boolean enabled; + + enabled = (data.get() != 0); + Log.d("ddm-heap", "REAQ says: enabled=" + enabled); + + client.getClientData().setAllocationStatus(enabled); + } + + /** + * Converts a VM class descriptor string ("Landroid/os/Debug;") to + * a dot-notation class name ("android.os.Debug"). + */ + private String descriptorToDot(String str) { + // count the number of arrays. + int array = 0; + while (str.startsWith("[")) { + str = str.substring(1); + array++; + } + + int len = str.length(); + + /* strip off leading 'L' and trailing ';' if appropriate */ + if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') { + str = str.substring(1, len-1); + str = str.replace('/', '.'); + } else { + // convert the basic types + if ("C".equals(str)) { + str = "char"; + } else if ("B".equals(str)) { + str = "byte"; + } else if ("Z".equals(str)) { + str = "boolean"; + } else if ("S".equals(str)) { + str = "short"; + } else if ("I".equals(str)) { + str = "int"; + } else if ("J".equals(str)) { + str = "long"; + } else if ("F".equals(str)) { + str = "float"; + } else if ("D".equals(str)) { + str = "double"; + } + } + + // now add the array part + for (int a = 0 ; a < array; a++) { + str = str + "[]"; + } + + return str; + } + + /** + * Reads a string table out of "data". + * + * This is just a serial collection of strings, each of which is a + * four-byte length followed by UTF-16 data. + */ + private void readStringTable(ByteBuffer data, String[] strings) { + int count = strings.length; + int i; + + for (i = 0; i < count; i++) { + int nameLen = data.getInt(); + String descriptor = getString(data, nameLen); + strings[i] = descriptorToDot(descriptor); + } + } + + /* + * Handle a REcent ALlocation response. + * + * Message header (all values big-endian): + * (1b) message header len (to allow future expansion); includes itself + * (1b) entry header len + * (1b) stack frame len + * (2b) number of entries + * (4b) offset to string table from start of message + * (2b) number of class name strings + * (2b) number of method name strings + * (2b) number of source file name strings + * For each entry: + * (4b) total allocation size + * (2b) threadId + * (2b) allocated object's class name index + * (1b) stack depth + * For each stack frame: + * (2b) method's class name + * (2b) method name + * (2b) method source file + * (2b) line number, clipped to 32767; -2 if native; -1 if no source + * (xb) class name strings + * (xb) method name strings + * (xb) source file strings + * + * As with other DDM traffic, strings are sent as a 4-byte length + * followed by UTF-16 data. + */ + private void handleREAL(Client client, ByteBuffer data) { + Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL)); + int messageHdrLen, entryHdrLen, stackFrameLen; + int numEntries, offsetToStrings; + int numClassNames, numMethodNames, numFileNames; + + /* + * Read the header. + */ + messageHdrLen = (data.get() & 0xff); + entryHdrLen = (data.get() & 0xff); + stackFrameLen = (data.get() & 0xff); + numEntries = (data.getShort() & 0xffff); + offsetToStrings = data.getInt(); + numClassNames = (data.getShort() & 0xffff); + numMethodNames = (data.getShort() & 0xffff); + numFileNames = (data.getShort() & 0xffff); + + + /* + * Skip forward to the strings and read them. + */ + data.position(offsetToStrings); + + String[] classNames = new String[numClassNames]; + String[] methodNames = new String[numMethodNames]; + String[] fileNames = new String[numFileNames]; + + readStringTable(data, classNames); + readStringTable(data, methodNames); + //System.out.println("METHODS: " + // + java.util.Arrays.deepToString(methodNames)); + readStringTable(data, fileNames); + + /* + * Skip back to a point just past the header and start reading + * entries. + */ + data.position(messageHdrLen); + + ArrayList<AllocationInfo> list = new ArrayList<AllocationInfo>(numEntries); + for (int i = 0; i < numEntries; i++) { + int totalSize; + int threadId, classNameIndex, stackDepth; + + totalSize = data.getInt(); + threadId = (data.getShort() & 0xffff); + classNameIndex = (data.getShort() & 0xffff); + stackDepth = (data.get() & 0xff); + /* we've consumed 9 bytes; gobble up any extra */ + for (int skip = 9; skip < entryHdrLen; skip++) + data.get(); + + StackTraceElement[] steArray = new StackTraceElement[stackDepth]; + + /* + * Pull out the stack trace. + */ + for (int sti = 0; sti < stackDepth; sti++) { + int methodClassNameIndex, methodNameIndex; + int methodSourceFileIndex; + short lineNumber; + String methodClassName, methodName, methodSourceFile; + + methodClassNameIndex = (data.getShort() & 0xffff); + methodNameIndex = (data.getShort() & 0xffff); + methodSourceFileIndex = (data.getShort() & 0xffff); + lineNumber = data.getShort(); + + methodClassName = classNames[methodClassNameIndex]; + methodName = methodNames[methodNameIndex]; + methodSourceFile = fileNames[methodSourceFileIndex]; + + steArray[sti] = new StackTraceElement(methodClassName, + methodName, methodSourceFile, lineNumber); + + /* we've consumed 8 bytes; gobble up any extra */ + for (int skip = 9; skip < stackFrameLen; skip++) + data.get(); + } + + list.add(new AllocationInfo(classNames[classNameIndex], + totalSize, (short) threadId, steArray)); + } + + // sort biggest allocations first. + Collections.sort(list); + + client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries])); + } + + /* + * For debugging: dump the contents of an AllocRecord array. + * + * The array starts with the oldest known allocation and ends with + * the most recent allocation. + */ + @SuppressWarnings("unused") + private static void dumpRecords(AllocationInfo[] records) { + System.out.println("Found " + records.length + " records:"); + + for (AllocationInfo rec: records) { + System.out.println("tid=" + rec.getThreadId() + " " + + rec.getAllocatedClass() + " (" + rec.getSize() + " bytes)"); + + for (StackTraceElement ste: rec.getStackTrace()) { + if (ste.isNativeMethod()) { + System.out.println(" " + ste.getClassName() + + "." + ste.getMethodName() + + " (Native method)"); + } else { + System.out.println(" " + ste.getClassName() + + "." + ste.getMethodName() + + " (" + ste.getFileName() + + ":" + ste.getLineNumber() + ")"); + } + } + } + } + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java new file mode 100644 index 0000000..5ba5aeb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "hello" chunk (HELO). + */ +final class HandleHello extends ChunkHandler { + + public static final int CHUNK_HELO = ChunkHandler.type("HELO"); + + private static final HandleHello mInst = new HandleHello(); + + + private HandleHello() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HELO, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-hello", "Now ready: " + client); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) { + Log.d("ddm-hello", "Now disconnected: " + client); + } + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-hello", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HELO) { + assert isReply; + handleHELO(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our HELO message. + */ + private static void handleHELO(Client client, ByteBuffer data) { + int version, pid, vmIdentLen, appNameLen; + String vmIdent, appName; + + version = data.getInt(); + pid = data.getInt(); + vmIdentLen = data.getInt(); + appNameLen = data.getInt(); + + vmIdent = getString(data, vmIdentLen); + appName = getString(data, appNameLen); + + Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid + + ", vm='" + vmIdent + "', app='" + appName + "'"); + + ClientData cd = client.getClientData(); + + synchronized (cd) { + if (cd.getPid() == pid) { + cd.setVmIdentifier(vmIdent); + cd.setClientDescription(appName); + cd.isDdmAware(true); + } else { + Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid (" + + cd.getPid() + ")"); + } + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + + + /** + * Send a HELO request to the client. + */ + public static void sendHELO(Client client, int serverProtocolVersion) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(serverProtocolVersion); + + finishChunkPacket(packet, CHUNK_HELO, buf.position()); + Log.d("ddm-hello", "Sending " + name(CHUNK_HELO) + + " ID=0x" + Integer.toHexString(packet.getId())); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java new file mode 100644 index 0000000..ca26590 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Handle thread status updates. + */ +final class HandleNativeHeap extends ChunkHandler { + + public static final int CHUNK_NHGT = type("NHGT"); // $NON-NLS-1$ + public static final int CHUNK_NHSG = type("NHSG"); // $NON-NLS-1$ + public static final int CHUNK_NHST = type("NHST"); // $NON-NLS-1$ + public static final int CHUNK_NHEN = type("NHEN"); // $NON-NLS-1$ + + private static final HandleNativeHeap mInst = new HandleNativeHeap(); + + private HandleNativeHeap() { + } + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_NHGT, mInst); + mt.registerChunkHandler(CHUNK_NHSG, mInst); + mt.registerChunkHandler(CHUNK_NHST, mInst); + mt.registerChunkHandler(CHUNK_NHEN, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_NHGT) { + handleNHGT(client, data); + } else if (type == CHUNK_NHST) { + // start chunk before any NHSG chunk(s) + client.getClientData().getNativeHeapData().clearHeapData(); + } else if (type == CHUNK_NHEN) { + // end chunk after NHSG chunk(s) + client.getClientData().getNativeHeapData().sealHeapData(); + } else if (type == CHUNK_NHSG) { + handleNHSG(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + client.update(Client.CHANGE_NATIVE_HEAP_DATA); + } + + /** + * Send an NHGT (Native Thread GeT) request to the client. + */ + public static void sendNHGT(Client client) throws IOException { + + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data in request message + + finishChunkPacket(packet, CHUNK_NHGT, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT)); + client.sendAndConsume(packet, mInst); + + rawBuf = allocBuffer(2); + packet = new JdwpPacket(rawBuf); + buf = getChunkDataBuf(rawBuf); + + buf.put((byte)HandleHeap.WHEN_GC); + buf.put((byte)HandleHeap.WHAT_OBJ); + + finishChunkPacket(packet, CHUNK_NHSG, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle our native heap data. + */ + private void handleNHGT(Client client, ByteBuffer data) { + ClientData cd = client.getClientData(); + + Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes"); + + // TODO - process incoming data and save in "cd" + byte[] copy = new byte[data.limit()]; + data.get(copy); + + // clear the previous run + cd.clearNativeAllocationInfo(); + + ByteBuffer buffer = ByteBuffer.wrap(copy); + buffer.order(ByteOrder.LITTLE_ENDIAN); + +// read the header +// typedef struct Header { +// uint32_t mapSize; +// uint32_t allocSize; +// uint32_t allocInfoSize; +// uint32_t totalMemory; +// uint32_t backtraceSize; +// }; + + int mapSize = buffer.getInt(); + int allocSize = buffer.getInt(); + int allocInfoSize = buffer.getInt(); + int totalMemory = buffer.getInt(); + int backtraceSize = buffer.getInt(); + + Log.d("ddms", "mapSize: " + mapSize); + Log.d("ddms", "allocSize: " + allocSize); + Log.d("ddms", "allocInfoSize: " + allocInfoSize); + Log.d("ddms", "totalMemory: " + totalMemory); + + cd.setTotalNativeMemory(totalMemory); + + // this means that updates aren't turned on. + if (allocInfoSize == 0) + return; + + if (mapSize > 0) { + byte[] maps = new byte[mapSize]; + buffer.get(maps, 0, mapSize); + parseMaps(cd, maps); + } + + int iterations = allocSize / allocInfoSize; + + for (int i = 0 ; i < iterations ; i++) { + NativeAllocationInfo info = new NativeAllocationInfo( + buffer.getInt() /* size */, + buffer.getInt() /* allocations */); + + for (int j = 0 ; j < backtraceSize ; j++) { + long addr = ((long)buffer.getInt()) & 0x00000000ffffffffL; + + info.addStackCallAddress(addr);; + } + + cd.addNativeAllocation(info); + } + } + + private void handleNHSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getNativeHeapData().addHeapData(data); + + if (true) { + return; + } + + // WORK IN PROGRESS + +// Log.e("ddm-nativeheap", "NHSG: ----------------------------------"); +// Log.e("ddm-nativeheap", "NHSG: " + data.limit() + " bytes"); + + byte[] copy = new byte[data.limit()]; + data.get(copy); + + ByteBuffer buffer = ByteBuffer.wrap(copy); + buffer.order(ByteOrder.BIG_ENDIAN); + + int id = buffer.getInt(); + int unitsize = (int) buffer.get(); + long startAddress = (long) buffer.getInt() & 0x00000000ffffffffL; + int offset = buffer.getInt(); + int allocationUnitCount = buffer.getInt(); + +// Log.e("ddm-nativeheap", "id: " + id); +// Log.e("ddm-nativeheap", "unitsize: " + unitsize); +// Log.e("ddm-nativeheap", "startAddress: 0x" + Long.toHexString(startAddress)); +// Log.e("ddm-nativeheap", "offset: " + offset); +// Log.e("ddm-nativeheap", "allocationUnitCount: " + allocationUnitCount); +// Log.e("ddm-nativeheap", "end: 0x" + +// Long.toHexString(startAddress + unitsize * allocationUnitCount)); + + // read the usage + while (buffer.position() < buffer.limit()) { + int eState = (int)buffer.get() & 0x000000ff; + int eLen = ((int)buffer.get() & 0x000000ff) + 1; + //Log.e("ddm-nativeheap", "solidity: " + (eState & 0x7) + " - kind: " + // + ((eState >> 3) & 0x7) + " - len: " + eLen); + } + + +// count += unitsize * allocationUnitCount; +// Log.e("ddm-nativeheap", "count = " + count); + + } + + private void parseMaps(ClientData cd, byte[] maps) { + InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps)); + BufferedReader reader = new BufferedReader(input); + + String line; + + try { + + // most libraries are defined on several lines, so we need to make sure we parse + // all the library lines and only add the library at the end + long startAddr = 0; + long endAddr = 0; + String library = null; + + while ((line = reader.readLine()) != null) { + Log.d("ddms", "line: " + line); + if (line.length() < 16) { + continue; + } + + try { + long tmpStart = Long.parseLong(line.substring(0, 8), 16); + long tmpEnd = Long.parseLong(line.substring(9, 17), 16); + + /* + * only check for library addresses as defined in + * //device/config/prelink-linux-arm.map + */ + if (tmpStart >= 0x0000000080000000L && tmpStart <= 0x00000000BFFFFFFFL) { + + int index = line.indexOf('/'); + + if (index == -1) + continue; + + String tmpLib = line.substring(index); + + if (library == null || + (library != null && tmpLib.equals(library) == false)) { + + if (library != null) { + cd.addNativeLibraryMapInfo(startAddr, endAddr, library); + Log.d("ddms", library + "(" + Long.toHexString(startAddr) + + " - " + Long.toHexString(endAddr) + ")"); + } + + // now init the new library + library = tmpLib; + startAddr = tmpStart; + endAddr = tmpEnd; + } else { + // add the new end + endAddr = tmpEnd; + } + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + if (library != null) { + cd.addNativeLibraryMapInfo(startAddr, endAddr, library); + Log.d("ddms", library + "(" + Long.toHexString(startAddr) + + " - " + Long.toHexString(endAddr) + ")"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java new file mode 100644 index 0000000..b9f3a74 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleTest extends ChunkHandler { + + public static final int CHUNK_TEST = type("TEST"); + + private static final HandleTest mInst = new HandleTest(); + + + private HandleTest() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_TEST, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-test", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_TEST) { + handleTEST(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + */ + private void handleTEST(Client client, ByteBuffer data) + { + /* + * Can't call data.array() on a read-only ByteBuffer, so we make + * a copy. + */ + byte[] copy = new byte[data.limit()]; + data.get(copy); + + Log.d("ddm-test", "Received:"); + Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java new file mode 100644 index 0000000..572eed2 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleThread extends ChunkHandler { + + public static final int CHUNK_THEN = type("THEN"); + public static final int CHUNK_THCR = type("THCR"); + public static final int CHUNK_THDE = type("THDE"); + public static final int CHUNK_THST = type("THST"); + public static final int CHUNK_THNM = type("THNM"); + public static final int CHUNK_STKL = type("STKL"); + + private static final HandleThread mInst = new HandleThread(); + + // only read/written by requestThreadUpdates() + private static volatile boolean mThreadStatusReqRunning = false; + private static volatile boolean mThreadStackTraceReqRunning = false; + + private HandleThread() {} + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_THCR, mInst); + mt.registerChunkHandler(CHUNK_THDE, mInst); + mt.registerChunkHandler(CHUNK_THST, mInst); + mt.registerChunkHandler(CHUNK_THNM, mInst); + mt.registerChunkHandler(CHUNK_STKL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-thread", "Now ready: " + client); + if (client.isThreadUpdateEnabled()) + sendTHEN(client, true); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-thread", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_THCR) { + handleTHCR(client, data); + } else if (type == CHUNK_THDE) { + handleTHDE(client, data); + } else if (type == CHUNK_THST) { + handleTHST(client, data); + } else if (type == CHUNK_THNM) { + handleTHNM(client, data); + } else if (type == CHUNK_STKL) { + handleSTKL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + * + * We should be tolerant of receiving a duplicate create message. (It + * shouldn't happen with the current implementation.) + */ + private void handleTHCR(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = getString(data, nameLen); + + Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'"); + + client.getClientData().addThread(threadId, name); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread death message. + */ + private void handleTHDE(Client client, ByteBuffer data) { + int threadId; + + threadId = data.getInt(); + Log.v("ddm-thread", "THDE: " + threadId); + + client.getClientData().removeThread(threadId); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread status update message. + * + * Response has: + * (1b) header len + * (1b) bytes per entry + * (2b) thread count + * Then, for each thread: + * (4b) threadId (matches value from THCR) + * (1b) thread status + * (4b) tid + * (4b) utime + * (4b) stime + */ + private void handleTHST(Client client, ByteBuffer data) { + int headerLen, bytesPerEntry, extraPerEntry; + int threadCount; + + headerLen = (data.get() & 0xff); + bytesPerEntry = (data.get() & 0xff); + threadCount = data.getShort(); + + headerLen -= 4; // we've read 4 bytes + while (headerLen-- > 0) + data.get(); + + extraPerEntry = bytesPerEntry - 18; // we want 18 bytes + + Log.v("ddm-thread", "THST: threadCount=" + threadCount); + + /* + * For each thread, extract the data, find the appropriate + * client, and add it to the ClientData. + */ + for (int i = 0; i < threadCount; i++) { + int threadId, status, tid, utime, stime; + boolean isDaemon = false; + + threadId = data.getInt(); + status = data.get(); + tid = data.getInt(); + utime = data.getInt(); + stime = data.getInt(); + if (bytesPerEntry >= 18) + isDaemon = (data.get() != 0); + + Log.v("ddm-thread", " id=" + threadId + + ", status=" + status + ", tid=" + tid + + ", utime=" + utime + ", stime=" + stime); + + ClientData cd = client.getClientData(); + ThreadInfo threadInfo = cd.getThread(threadId); + if (threadInfo != null) + threadInfo.updateThread(status, tid, utime, stime, isDaemon); + else + Log.i("ddms", "Thread with id=" + threadId + " not found"); + + // slurp up any extra + for (int slurp = extraPerEntry; slurp > 0; slurp--) + data.get(); + } + + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a THNM (THread NaMe) message. We get one of these after + * somebody calls Thread.setName() on a running thread. + */ + private void handleTHNM(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = getString(data, nameLen); + + Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'"); + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setThreadName(name); + client.update(Client.CHANGE_THREAD_DATA); + } else { + Log.i("ddms", "Thread with id=" + threadId + " not found"); + } + } + + + /** + * Parse an incoming STKL. + */ + private void handleSTKL(Client client, ByteBuffer data) { + StackTraceElement[] trace; + int i, threadId, stackDepth; + @SuppressWarnings("unused") + int future; + + future = data.getInt(); + threadId = data.getInt(); + + Log.v("ddms", "STKL: " + threadId); + + /* un-serialize the StackTraceElement[] */ + stackDepth = data.getInt(); + trace = new StackTraceElement[stackDepth]; + for (i = 0; i < stackDepth; i++) { + String className, methodName, fileName; + int len, lineNumber; + + len = data.getInt(); + className = getString(data, len); + len = data.getInt(); + methodName = getString(data, len); + len = data.getInt(); + if (len == 0) { + fileName = null; + } else { + fileName = getString(data, len); + } + lineNumber = data.getInt(); + + trace[i] = new StackTraceElement(className, methodName, fileName, + lineNumber); + } + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setStackCall(trace); + client.update(Client.CHANGE_THREAD_STACKTRACE); + } else { + Log.d("STKL", String.format( + "Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$ + threadId)); + } + } + + + /** + * Send a THEN (THread notification ENable) request to the client. + */ + public static void sendTHEN(Client client, boolean enable) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + if (enable) + buf.put((byte)1); + else + buf.put((byte)0); + + finishChunkPacket(packet, CHUNK_THEN, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + + /** + * Send a STKL (STacK List) request to the client. The VM will suspend + * the target thread, obtain its stack, and return it. If the thread + * is no longer running, a failure result will be returned. + */ + public static void sendSTKL(Client client, int threadId) + throws IOException { + + if (false) { + Log.i("ddm-thread", "would send STKL " + threadId); + return; + } + + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(threadId); + + finishChunkPacket(packet, CHUNK_STKL, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId); + client.sendAndConsume(packet, mInst); + } + + + /** + * This is called periodically from the UI thread. To avoid locking + * the UI while we request the updates, we create a new thread. + * + */ + static void requestThreadUpdate(final Client client) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (mThreadStatusReqRunning) { + Log.w("ddms", "Waiting for previous thread update req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + mThreadStatusReqRunning = true; + try { + sendTHST(client); + } catch (IOException ioe) { + Log.i("ddms", "Unable to request thread updates from " + + client + ": " + ioe.getMessage()); + } finally { + mThreadStatusReqRunning = false; + } + } + }.start(); + } + } + + static void requestThreadStackCallRefresh(final Client client, final int threadId) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (mThreadStackTraceReqRunning ) { + Log.w("ddms", "Waiting for previous thread stack call req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + mThreadStackTraceReqRunning = true; + try { + sendSTKL(client, threadId); + } catch (IOException ioe) { + Log.i("ddms", "Unable to request thread stack call updates from " + + client + ": " + ioe.getMessage()); + } finally { + mThreadStackTraceReqRunning = false; + } + } + }.start(); + } + + } + + /* + * Send a THST request to the specified client. + */ + private static void sendTHST(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // nothing much to say + + finishChunkPacket(packet, CHUNK_THST, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THST)); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java new file mode 100644 index 0000000..d27e636 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "wait" chunk (WAIT). These are sent up when the client is + * waiting for something, e.g. for a debugger to attach. + */ +final class HandleWait extends ChunkHandler { + + public static final int CHUNK_WAIT = ChunkHandler.type("WAIT"); + + private static final HandleWait mInst = new HandleWait(); + + + private HandleWait() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_WAIT, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-wait", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_WAIT) { + assert !isReply; + handleWAIT(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our WAIT message. + */ + private static void handleWAIT(Client client, ByteBuffer data) { + byte reason; + + reason = data.get(); + + Log.i("ddm-wait", "WAIT: reason=" + reason); + + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_WAITING); + } + + client.update(Client.CHANGE_DEBUGGER_INTEREST); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java new file mode 100644 index 0000000..6a62e60 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.text.ParseException; + +/** + * Describes the types and locations of objects in a segment of a heap. + */ +public final class HeapSegment implements Comparable<HeapSegment> { + + /** + * Describes an object/region encoded in the HPSG data. + */ + public static class HeapSegmentElement implements Comparable<HeapSegmentElement> { + + /* + * Solidity values, which must match the values in + * the HPSG data. + */ + + /** The element describes a free block. */ + public static int SOLIDITY_FREE = 0; + + /** The element is strongly-reachable. */ + public static int SOLIDITY_HARD = 1; + + /** The element is softly-reachable. */ + public static int SOLIDITY_SOFT = 2; + + /** The element is weakly-reachable. */ + public static int SOLIDITY_WEAK = 3; + + /** The element is phantom-reachable. */ + public static int SOLIDITY_PHANTOM = 4; + + /** The element is pending finalization. */ + public static int SOLIDITY_FINALIZABLE = 5; + + /** The element is not reachable, and is about to be swept/freed. */ + public static int SOLIDITY_SWEEP = 6; + + /** The reachability of the object is unknown. */ + public static int SOLIDITY_INVALID = -1; + + + /* + * Kind values, which must match the values in + * the HPSG data. + */ + + /** The element describes a data object. */ + public static int KIND_OBJECT = 0; + + /** The element describes a class object. */ + public static int KIND_CLASS_OBJECT = 1; + + /** The element describes an array of 1-byte elements. */ + public static int KIND_ARRAY_1 = 2; + + /** The element describes an array of 2-byte elements. */ + public static int KIND_ARRAY_2 = 3; + + /** The element describes an array of 4-byte elements. */ + public static int KIND_ARRAY_4 = 4; + + /** The element describes an array of 8-byte elements. */ + public static int KIND_ARRAY_8 = 5; + + /** The element describes an unknown type of object. */ + public static int KIND_UNKNOWN = 6; + + /** The element describes a native object. */ + public static int KIND_NATIVE = 7; + + /** The object kind is unknown or unspecified. */ + public static int KIND_INVALID = -1; + + + /** + * A bit in the HPSG data that indicates that an element should + * be combined with the element that follows, typically because + * an element is too large to be described by a single element. + */ + private static int PARTIAL_MASK = 1 << 7; + + + /** + * Describes the reachability/solidity of the element. Must + * be set to one of the SOLIDITY_* values. + */ + private int mSolidity; + + /** + * Describes the type/kind of the element. Must be set to one + * of the KIND_* values. + */ + private int mKind; + + /** + * Describes the length of the element, in bytes. + */ + private int mLength; + + + /** + * Creates an uninitialized element. + */ + public HeapSegmentElement() { + setSolidity(SOLIDITY_INVALID); + setKind(KIND_INVALID); + setLength(-1); + } + + /** + * Create an element describing the entry at the current + * position of hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position + * of hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement(HeapSegment hs) + throws BufferUnderflowException, ParseException { + set(hs); + } + + /** + * Replace the element with the entry at the current position of + * hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @return this object. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position of + * hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement set(HeapSegment hs) + throws BufferUnderflowException, ParseException { + + /* TODO: Maybe keep track of the virtual address of each element + * so that they can be examined independently. + */ + ByteBuffer data = hs.mUsageData; + int eState = (int)data.get() & 0x000000ff; + int eLen = ((int)data.get() & 0x000000ff) + 1; + + while ((eState & PARTIAL_MASK) != 0) { + + /* If the partial bit was set, the next byte should describe + * the same object as the current one. + */ + int nextState = (int)data.get() & 0x000000ff; + if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) { + throw new ParseException("State mismatch", data.position()); + } + eState = nextState; + eLen += ((int)data.get() & 0x000000ff) + 1; + } + + setSolidity(eState & 0x7); + setKind((eState >> 3) & 0x7); + setLength(eLen * hs.mAllocationUnitSize); + + return this; + } + + public int getSolidity() { + return mSolidity; + } + + public void setSolidity(int solidity) { + this.mSolidity = solidity; + } + + public int getKind() { + return mKind; + } + + public void setKind(int kind) { + this.mKind = kind; + } + + public int getLength() { + return mLength; + } + + public void setLength(int length) { + this.mLength = length; + } + + public int compareTo(HeapSegmentElement other) { + if (mLength != other.mLength) { + return mLength < other.mLength ? -1 : 1; + } + return 0; + } + } + + //* The ID of the heap that this segment belongs to. + protected int mHeapId; + + //* The size of an allocation unit, in bytes. (e.g., 8 bytes) + protected int mAllocationUnitSize; + + //* The virtual address of the start of this segment. + protected long mStartAddress; + + //* The offset of this pices from mStartAddress, in bytes. + protected int mOffset; + + //* The number of allocation units described in this segment. + protected int mAllocationUnitCount; + + //* The raw data that describes the contents of this segment. + protected ByteBuffer mUsageData; + + //* mStartAddress is set to this value when the segment becomes invalid. + private final static long INVALID_START_ADDRESS = -1; + + /** + * Create a new HeapSegment based on the raw contents + * of an HPSG chunk. + * + * @param hpsgData The raw data from an HPSG chunk. + * @throws BufferUnderflowException if hpsgData is too small + * to hold the HPSG chunk header data. + */ + public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException { + /* Read the HPSG chunk header. + * These get*() calls may throw a BufferUnderflowException + * if the underlying data isn't big enough. + */ + hpsgData.order(ByteOrder.BIG_ENDIAN); + mHeapId = hpsgData.getInt(); + mAllocationUnitSize = (int) hpsgData.get(); + mStartAddress = (long) hpsgData.getInt() & 0x00000000ffffffffL; + mOffset = hpsgData.getInt(); + mAllocationUnitCount = hpsgData.getInt(); + + // Hold onto the remainder of the data. + mUsageData = hpsgData.slice(); + mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter + + // Validate the data. +//xxx do it +//xxx make sure the number of elements matches mAllocationUnitCount. +//xxx make sure the last element doesn't have P set + } + + /** + * See if this segment still contains data, and has not been + * appended to another segment. + * + * @return true if this segment has not been appended to + * another segment. + */ + public boolean isValid() { + return mStartAddress != INVALID_START_ADDRESS; + } + + /** + * See if <code>other</code> comes immediately after this segment. + * + * @param other The HeapSegment to check. + * @return true if <code>other</code> comes immediately after this + * segment. + */ + public boolean canAppend(HeapSegment other) { + return isValid() && other.isValid() && mHeapId == other.mHeapId && + mAllocationUnitSize == other.mAllocationUnitSize && + getEndAddress() == other.getStartAddress(); + } + + /** + * Append the contents of <code>other</code> to this segment + * if it describes the segment immediately after this one. + * + * @param other The segment to append to this segment, if possible. + * If appended, <code>other</code> will be invalid + * when this method returns. + * @return true if <code>other</code> was successfully appended to + * this segment. + */ + public boolean append(HeapSegment other) { + if (canAppend(other)) { + /* Preserve the position. The mark is not preserved, + * but we don't use it anyway. + */ + int pos = mUsageData.position(); + + // Guarantee that we have enough room for the new data. + if (mUsageData.capacity() - mUsageData.limit() < + other.mUsageData.limit()) { + /* Grow more than necessary in case another append() + * is about to happen. + */ + int newSize = mUsageData.limit() + other.mUsageData.limit(); + ByteBuffer newData = ByteBuffer.allocate(newSize * 2); + + mUsageData.rewind(); + newData.put(mUsageData); + mUsageData = newData; + } + + // Copy the data from the other segment and restore the position. + other.mUsageData.rewind(); + mUsageData.put(other.mUsageData); + mUsageData.position(pos); + + // Fix this segment's header to cover the new data. + mAllocationUnitCount += other.mAllocationUnitCount; + + // Mark the other segment as invalid. + other.mStartAddress = INVALID_START_ADDRESS; + other.mUsageData = null; + + return true; + } else { + return false; + } + } + + public long getStartAddress() { + return mStartAddress + mOffset; + } + + public int getLength() { + return mAllocationUnitSize * mAllocationUnitCount; + } + + public long getEndAddress() { + return getStartAddress() + getLength(); + } + + public void rewindElements() { + if (mUsageData != null) { + mUsageData.rewind(); + } + } + + public HeapSegmentElement getNextElement(HeapSegmentElement reuse) { + try { + if (reuse != null) { + return reuse.set(this); + } else { + return new HeapSegmentElement(this); + } + } catch (BufferUnderflowException ex) { + /* Normal "end of buffer" situation. + */ + } catch (ParseException ex) { + /* Malformed data. + */ +//TODO: we should catch this in the constructor + } + return null; + } + + /* + * Method overrides for Comparable + */ + @Override + public boolean equals(Object o) { + if (o instanceof HeapSegment) { + return compareTo((HeapSegment) o) == 0; + } + return false; + } + + @Override + public int hashCode() { + return mHeapId * 31 + + mAllocationUnitSize * 31 + + (int) mStartAddress * 31 + + mOffset * 31 + + mAllocationUnitCount * 31 + + mUsageData.hashCode(); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("HeapSegment { heap ").append(mHeapId) + .append(", start 0x") + .append(Integer.toHexString((int) getStartAddress())) + .append(", length ").append(getLength()) + .append(" }"); + + return str.toString(); + } + + public int compareTo(HeapSegment other) { + if (mHeapId != other.mHeapId) { + return mHeapId < other.mHeapId ? -1 : 1; + } + if (getStartAddress() != other.getStartAddress()) { + return getStartAddress() < other.getStartAddress() ? -1 : 1; + } + + /* If two segments have the same start address, the rest of + * the fields should be equal. Go through the motions, though. + * Note that we re-check the components of getStartAddress() + * (mStartAddress and mOffset) to make sure that all fields in + * an equal segment are equal. + */ + + if (mAllocationUnitSize != other.mAllocationUnitSize) { + return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1; + } + if (mStartAddress != other.mStartAddress) { + return mStartAddress < other.mStartAddress ? -1 : 1; + } + if (mOffset != other.mOffset) { + return mOffset < other.mOffset ? -1 : 1; + } + if (mAllocationUnitCount != other.mAllocationUnitCount) { + return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1; + } + if (mUsageData != other.mUsageData) { + return mUsageData.compareTo(other.mUsageData); + } + return 0; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java new file mode 100755 index 0000000..5dbce92 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.Device.DeviceState; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.util.Map; + + +/** + * A Device. It can be a physical device or an emulator. + */ +public interface IDevice { + + public final static String PROP_BUILD_VERSION = "ro.build.version.release"; + public final static String PROP_BUILD_VERSION_NUMBER = "ro.build.version.sdk"; + public final static String PROP_DEBUGGABLE = "ro.debuggable"; + /** Serial number of the first connected emulator. */ + public final static String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$ + /** Device change bit mask: {@link DeviceState} change. */ + public static final int CHANGE_STATE = 0x0001; + /** Device change bit mask: {@link Client} list change. */ + public static final int CHANGE_CLIENT_LIST = 0x0002; + /** Device change bit mask: build info change. */ + public static final int CHANGE_BUILD_INFO = 0x0004; + + /** + * Returns the serial number of the device. + */ + public String getSerialNumber(); + + /** + * Returns the name of the AVD the emulator is running. + * <p/>This is only valid if {@link #isEmulator()} returns true. + * <p/>If the emulator is not running any AVD (for instance it's running from an Android source + * tree build), this method will return "<code><build></code>". + * @return the name of the AVD or <code>null</code> if there isn't any. + */ + public String getAvdName(); + + /** + * Returns the state of the device. + */ + public DeviceState getState(); + + /** + * Returns the device properties. It contains the whole output of 'getprop' + */ + public Map<String, String> getProperties(); + + /** + * Returns the number of property for this device. + */ + public int getPropertyCount(); + + /** + * Returns a property value. + * @param name the name of the value to return. + * @return the value or <code>null</code> if the property does not exist. + */ + public String getProperty(String name); + + /** + * Returns if the device is ready. + * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#ONLINE}. + */ + public boolean isOnline(); + + /** + * Returns <code>true</code> if the device is an emulator. + */ + public boolean isEmulator(); + + /** + * Returns if the device is offline. + * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#OFFLINE}. + */ + public boolean isOffline(); + + /** + * Returns if the device is in bootloader mode. + * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#BOOTLOADER}. + */ + public boolean isBootLoader(); + + /** + * Returns whether the {@link Device} has {@link Client}s. + */ + public boolean hasClients(); + + /** + * Returns the array of clients. + */ + public Client[] getClients(); + + /** + * Returns a {@link Client} by its application name. + * @param applicationName the name of the application + * @return the <code>Client</code> object or <code>null</code> if no match was found. + */ + public Client getClient(String applicationName); + + /** + * Returns a {@link SyncService} object to push / pull files to and from the device. + * @return <code>null</code> if the SyncService couldn't be created. + */ + public SyncService getSyncService(); + + /** + * Returns a {@link FileListingService} for this device. + */ + public FileListingService getFileListingService(); + + /** + * Takes a screen shot of the device and returns it as a {@link RawImage}. + * @return the screenshot as a <code>RawImage</code> or <code>null</code> if + * something went wrong. + * @throws IOException + */ + public RawImage getScreenshot() throws IOException; + + /** + * Executes a shell command on the device, and sends the result to a receiver. + * @param command The command to execute + * @param receiver The receiver object getting the result from the command. + * @throws IOException + */ + public void executeShellCommand(String command, + IShellOutputReceiver receiver) throws IOException; + + /** + * Runs the event log service and outputs the event log to the {@link LogReceiver}. + * @param receiver the receiver to receive the event log entries. + * @throws IOException + */ + public void runEventLogService(LogReceiver receiver) throws IOException; + + /** + * Runs the log service for the given log and outputs the log to the {@link LogReceiver}. + * @param logname the logname of the log to read from. + * @param receiver the receiver to receive the event log entries. + * @throws IOException + */ + public void runLogService(String logname, LogReceiver receiver) throws IOException; + + /** + * Creates a port forwarding between a local and a remote port. + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return <code>true</code> if success. + */ + public boolean createForward(int localPort, int remotePort); + + /** + * Removes a port forwarding between a local and a remote port. + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return <code>true</code> if success. + */ + public boolean removeForward(int localPort, int remotePort); + + /** + * Returns the name of the client by pid or <code>null</code> if pid is unknown + * @param pid the pid of the client. + */ + public String getClientName(int pid); + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java new file mode 100644 index 0000000..fb671bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Classes which implement this interface provide methods that deal with out from a remote shell + * command on a device/emulator. + */ +public interface IShellOutputReceiver { + /** + * Called every time some new data is available. + * @param data The new data. + * @param offset The offset at which the new data starts. + * @param length The length of the new data. + */ + public void addOutput(byte[] data, int offset, int length); + + /** + * Called at the end of the process execution (unless the process was + * canceled). This allows the receiver to terminate and flush whatever + * data was not yet processed. + */ + public void flush(); + + /** + * Cancel method to stop the execution of the remote shell command. + * @return true to cancel the execution of the command. + */ + public boolean isCancelled(); +}; diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java new file mode 100644 index 0000000..3b9d730 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Classes which implement this interface provide a method that returns a stack trace. + */ +public interface IStackTraceInfo { + + /** + * Returns the stack trace. This can be <code>null</code>. + */ + public StackTraceElement[] getStackTrace(); + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java new file mode 100644 index 0000000..92bbb82 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java @@ -0,0 +1,371 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; + +/** + * A JDWP packet, sitting at the start of a ByteBuffer somewhere. + * + * This allows us to wrap a "pointer" to the data with the results of + * decoding the packet. + * + * None of the operations here are synchronized. If multiple threads will + * be accessing the same ByteBuffers, external sync will be required. + * + * Use the constructor to create an empty packet, or "findPacket()" to + * wrap a JdwpPacket around existing data. + */ +final class JdwpPacket { + // header len + public static final int JDWP_HEADER_LEN = 11; + + // results from findHandshake + public static final int HANDSHAKE_GOOD = 1; + public static final int HANDSHAKE_NOTYET = 2; + public static final int HANDSHAKE_BAD = 3; + + // our cmdSet/cmd + private static final int DDMS_CMD_SET = 0xc7; // 'G' + 128 + private static final int DDMS_CMD = 0x01; + + // "flags" field + private static final int REPLY_PACKET = 0x80; + + // this is sent and expected at the start of a JDWP connection + private static final byte[] mHandshake = { + 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e' + }; + + public static final int HANDSHAKE_LEN = mHandshake.length; + + private ByteBuffer mBuffer; + private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode; + private boolean mIsNew; + + private static int mSerialId = 0x40000000; + + + /** + * Create a new, empty packet, in "buf". + */ + JdwpPacket(ByteBuffer buf) { + mBuffer = buf; + mIsNew = true; + } + + /** + * Finish a packet created with newPacket(). + * + * This always creates a command packet, with the next serial number + * in sequence. + * + * We have to take "payloadLength" as an argument because we can't + * see the position in the "slice" returned by getPayload(). We could + * fish it out of the chunk header, but it's legal for there to be + * more than one chunk in a JDWP packet. + * + * On exit, "position" points to the end of the data. + */ + void finishPacket(int payloadLength) { + assert mIsNew; + + ByteOrder oldOrder = mBuffer.order(); + mBuffer.order(ChunkHandler.CHUNK_ORDER); + + mLength = JDWP_HEADER_LEN + payloadLength; + mId = getNextSerial(); + mFlags = 0; + mCmdSet = DDMS_CMD_SET; + mCmd = DDMS_CMD; + + mBuffer.putInt(0x00, mLength); + mBuffer.putInt(0x04, mId); + mBuffer.put(0x08, (byte) mFlags); + mBuffer.put(0x09, (byte) mCmdSet); + mBuffer.put(0x0a, (byte) mCmd); + + mBuffer.order(oldOrder); + mBuffer.position(mLength); + } + + /** + * Get the next serial number. This creates a unique serial number + * across all connections, not just for the current connection. This + * is a useful property when debugging, but isn't necessary. + * + * We can't synchronize on an int, so we use a sync method. + */ + private static synchronized int getNextSerial() { + return mSerialId++; + } + + /** + * Return a slice of the byte buffer, positioned past the JDWP header + * to the start of the chunk header. The buffer's limit will be set + * to the size of the payload if the size is known; if this is a + * packet under construction the limit will be set to the end of the + * buffer. + * + * Doesn't examine the packet at all -- works on empty buffers. + */ + ByteBuffer getPayload() { + ByteBuffer buf; + int oldPosn = mBuffer.position(); + + mBuffer.position(JDWP_HEADER_LEN); + buf = mBuffer.slice(); // goes from position to limit + mBuffer.position(oldPosn); + + if (mLength > 0) + buf.limit(mLength - JDWP_HEADER_LEN); + else + assert mIsNew; + buf.order(ChunkHandler.CHUNK_ORDER); + return buf; + } + + /** + * Returns "true" if this JDWP packet has a JDWP command type. + * + * This never returns "true" for reply packets. + */ + boolean isDdmPacket() { + return (mFlags & REPLY_PACKET) == 0 && + mCmdSet == DDMS_CMD_SET && + mCmd == DDMS_CMD; + } + + /** + * Returns "true" if this JDWP packet is tagged as a reply. + */ + boolean isReply() { + return (mFlags & REPLY_PACKET) != 0; + } + + /** + * Returns "true" if this JDWP packet is a reply with a nonzero + * error code. + */ + boolean isError() { + return isReply() && mErrCode != 0; + } + + /** + * Returns "true" if this JDWP packet has no data. + */ + boolean isEmpty() { + return (mLength == JDWP_HEADER_LEN); + } + + /** + * Return the packet's ID. For a reply packet, this allows us to + * match the reply with the original request. + */ + int getId() { + return mId; + } + + /** + * Return the length of a packet. This includes the header, so an + * empty packet is 11 bytes long. + */ + int getLength() { + return mLength; + } + + /** + * Write our packet to "chan". Consumes the packet as part of the + * write. + * + * The JDWP packet starts at offset 0 and ends at mBuffer.position(). + */ + void writeAndConsume(SocketChannel chan) throws IOException { + int oldLimit; + + //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + assert mLength > 0; + + mBuffer.flip(); // limit<-posn, posn<-0 + oldLimit = mBuffer.limit(); + mBuffer.limit(mLength); + while (mBuffer.position() != mBuffer.limit()) { + chan.write(mBuffer); + } + // position should now be at end of packet + assert mBuffer.position() == mLength; + + mBuffer.limit(oldLimit); + mBuffer.compact(); // shift posn...limit, posn<-pending data + + //Log.i("ddms", " : pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * "Move" the packet data out of the buffer we're sitting on and into + * buf at the current position. + */ + void movePacket(ByteBuffer buf) { + Log.v("ddms", "moving " + mLength + " bytes"); + int oldPosn = mBuffer.position(); + + mBuffer.position(0); + mBuffer.limit(mLength); + buf.put(mBuffer); + mBuffer.position(mLength); + mBuffer.limit(oldPosn); + mBuffer.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Consume the JDWP packet. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + void consume() + { + //Log.d("ddms", "consuming " + mLength + " bytes"); + //Log.d("ddms", " posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + /* + * The "flip" call sets "limit" equal to the position (usually the + * end of data) and "position" equal to zero. + * + * compact() copies everything from "position" and "limit" to the + * start of the buffer, sets "position" to the end of data, and + * sets "limit" to the capacity. + * + * On entry, "position" is set to the amount of data in the buffer + * and "limit" is set to the capacity. We want to call flip() + * so that position..limit spans our data, advance "position" past + * the current packet, then compact. + */ + mBuffer.flip(); // limit<-posn, posn<-0 + mBuffer.position(mLength); + mBuffer.compact(); // shift posn...limit, posn<-pending data + mLength = 0; + //Log.d("ddms", " after compact, posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * Find the JDWP packet at the start of "buf". The start is known, + * but the length has to be parsed out. + * + * On entry, the packet data in "buf" must start at offset 0 and end + * at "position". "limit" should be set to the buffer capacity. This + * method does not alter "buf"s attributes. + * + * Returns a new JdwpPacket if a full one is found in the buffer. If + * not, returns null. Throws an exception if the data doesn't look like + * a valid JDWP packet. + */ + static JdwpPacket findPacket(ByteBuffer buf) { + int count = buf.position(); + int length, id, flags, cmdSet, cmd; + + if (count < JDWP_HEADER_LEN) + return null; + + ByteOrder oldOrder = buf.order(); + buf.order(ChunkHandler.CHUNK_ORDER); + + length = buf.getInt(0x00); + id = buf.getInt(0x04); + flags = buf.get(0x08) & 0xff; + cmdSet = buf.get(0x09) & 0xff; + cmd = buf.get(0x0a) & 0xff; + + buf.order(oldOrder); + + if (length < JDWP_HEADER_LEN) + throw new BadPacketException(); + if (count < length) + return null; + + JdwpPacket pkt = new JdwpPacket(buf); + //pkt.mBuffer = buf; + pkt.mLength = length; + pkt.mId = id; + pkt.mFlags = flags; + + if ((flags & REPLY_PACKET) == 0) { + pkt.mCmdSet = cmdSet; + pkt.mCmd = cmd; + pkt.mErrCode = -1; + } else { + pkt.mCmdSet = -1; + pkt.mCmd = -1; + pkt.mErrCode = cmdSet | (cmd << 8); + } + + return pkt; + } + + /** + * Like findPacket(), but when we're expecting the JDWP handshake. + * + * Returns one of: + * HANDSHAKE_GOOD - found handshake, looks good + * HANDSHAKE_BAD - found enough data, but it's wrong + * HANDSHAKE_NOTYET - not enough data has been read yet + */ + static int findHandshake(ByteBuffer buf) { + int count = buf.position(); + int i; + + if (count < mHandshake.length) + return HANDSHAKE_NOTYET; + + for (i = mHandshake.length -1; i >= 0; --i) { + if (buf.get(i) != mHandshake[i]) + return HANDSHAKE_BAD; + } + + return HANDSHAKE_GOOD; + } + + /** + * Remove the handshake string from the buffer. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + static void consumeHandshake(ByteBuffer buf) { + // in theory, nothing else can have arrived, so this is overkill + buf.flip(); // limit<-posn, posn<-0 + buf.position(mHandshake.length); + buf.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Copy the handshake string into the output buffer. + * + * On exit, "buf"s position will be advanced. + */ + static void putHandshake(ByteBuffer buf) { + buf.put(mHandshake); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java new file mode 100644 index 0000000..ce95b04 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Log class that mirrors the API in main Android sources. + * <p/>Default behavior outputs the log to {@link System#out}. Use + * {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else. + */ +public final class Log { + + /** + * Log Level enum. + */ + public enum LogLevel { + VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$ + DEBUG(3, "debug", 'D'), //$NON-NLS-1$ + INFO(4, "info", 'I'), //$NON-NLS-1$ + WARN(5, "warn", 'W'), //$NON-NLS-1$ + ERROR(6, "error", 'E'), //$NON-NLS-1$ + ASSERT(7, "assert", 'A'); //$NON-NLS-1$ + + private int mPriorityLevel; + private String mStringValue; + private char mPriorityLetter; + + LogLevel(int intPriority, String stringValue, char priorityChar) { + mPriorityLevel = intPriority; + mStringValue = stringValue; + mPriorityLetter = priorityChar; + } + + public static LogLevel getByString(String value) { + for (LogLevel mode : values()) { + if (mode.mStringValue.equals(value)) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + * @param letter the letter matching a <code>LogLevel</code> enum + * @return a <code>LogLevel</code> object or <code>null</code> if no match were found. + */ + public static LogLevel getByLetter(char letter) { + for (LogLevel mode : values()) { + if (mode.mPriorityLetter == letter) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + * <p/> + * The letter is passed as a {@link String} argument, but only the first character + * is used. + * @param letter the letter matching a <code>LogLevel</code> enum + * @return a <code>LogLevel</code> object or <code>null</code> if no match were found. + */ + public static LogLevel getByLetterString(String letter) { + if (letter.length() > 0) { + return getByLetter(letter.charAt(0)); + } + + return null; + } + + /** + * Returns the letter identifying the priority of the {@link LogLevel}. + */ + public char getPriorityLetter() { + return mPriorityLetter; + } + + /** + * Returns the numerical value of the priority. + */ + public int getPriority() { + return mPriorityLevel; + } + + /** + * Returns a non translated string representing the LogLevel. + */ + public String getStringValue() { + return mStringValue; + } + } + + /** + * Classes which implement this interface provides methods that deal with outputting log + * messages. + */ + public interface ILogOutput { + /** + * Sent when a log message needs to be printed. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printLog(LogLevel logLevel, String tag, String message); + + /** + * Sent when a log message needs to be printed, and, if possible, displayed to the user + * in a dialog box. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printAndPromptLog(LogLevel logLevel, String tag, String message); + } + + private static LogLevel mLevel = DdmPreferences.getLogLevel(); + + private static ILogOutput sLogOutput; + + private static final char[] mSpaceLine = new char[72]; + private static final char[] mHexDigit = new char[] + { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + static { + /* prep for hex dump */ + int i = mSpaceLine.length-1; + while (i >= 0) + mSpaceLine[i--] = ' '; + mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0'; + mSpaceLine[4] = '-'; + } + + static final class Config { + static final boolean LOGV = true; + static final boolean LOGD = true; + }; + + private Log() {} + + /** + * Outputs a {@link LogLevel#VERBOSE} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void v(String tag, String message) { + println(LogLevel.VERBOSE, tag, message); + } + + /** + * Outputs a {@link LogLevel#DEBUG} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void d(String tag, String message) { + println(LogLevel.DEBUG, tag, message); + } + + /** + * Outputs a {@link LogLevel#INFO} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void i(String tag, String message) { + println(LogLevel.INFO, tag, message); + } + + /** + * Outputs a {@link LogLevel#WARN} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void w(String tag, String message) { + println(LogLevel.WARN, tag, message); + } + + /** + * Outputs a {@link LogLevel#ERROR} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void e(String tag, String message) { + println(LogLevel.ERROR, tag, message); + } + + /** + * Outputs a log message and attempts to display it in a dialog. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void logAndDisplay(LogLevel logLevel, String tag, String message) { + if (sLogOutput != null) { + sLogOutput.printAndPromptLog(logLevel, tag, message); + } else { + println(logLevel, tag, message); + } + } + + /** + * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information. + * @param tag The tag associated with the message. + * @param throwable The {@link Throwable} to output. + */ + public static void e(String tag, Throwable throwable) { + if (throwable != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + throwable.printStackTrace(pw); + println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString()); + } + } + + static void setLevel(LogLevel logLevel) { + mLevel = logLevel; + } + + /** + * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out} + * will be used. + * @param logOutput The {@link ILogOutput} to use to print the log. + */ + public static void setLogOutput(ILogOutput logOutput) { + sLogOutput = logOutput; + } + + /** + * Show hex dump. + * <p/> + * Local addition. Output looks like: + * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef + * <p/> + * Uses no string concatenation; creates one String object per line. + */ + static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) { + + int kHexOffset = 6; + int kAscOffset = 55; + char[] line = new char[mSpaceLine.length]; + int addr, baseAddr, count; + int i, ch; + boolean needErase = true; + + //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length); + + baseAddr = 0; + while (length != 0) { + if (length > 16) { + // full line + count = 16; + } else { + // partial line; re-copy blanks to clear end + count = length; + needErase = true; + } + + if (needErase) { + System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length); + needErase = false; + } + + // output the address (currently limited to 4 hex digits) + addr = baseAddr; + addr &= 0xffff; + ch = 3; + while (addr != 0) { + line[ch] = mHexDigit[addr & 0x0f]; + ch--; + addr >>>= 4; + } + + // output hex digits and ASCII chars + ch = kHexOffset; + for (i = 0; i < count; i++) { + byte val = data[offset + i]; + + line[ch++] = mHexDigit[(val >>> 4) & 0x0f]; + line[ch++] = mHexDigit[val & 0x0f]; + ch++; + + if (val >= 0x20 && val < 0x7f) + line[kAscOffset + i] = (char) val; + else + line[kAscOffset + i] = '.'; + } + + println(level, tag, new String(line)); + + // advance to next chunk of data + length -= count; + offset += count; + baseAddr += count; + } + + } + + /** + * Dump the entire contents of a byte array with DEBUG priority. + */ + static void hexDump(byte[] data) { + hexDump("ddms", LogLevel.DEBUG, data, 0, data.length); + } + + /* currently prints to stdout; could write to a log window */ + private static void println(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= mLevel.getPriority()) { + if (sLogOutput != null) { + sLogOutput.printLog(logLevel, tag, message); + } else { + printLog(logLevel, tag, message); + } + } + } + + /** + * Prints a log message. + * @param logLevel + * @param tag + * @param message + */ + public static void printLog(LogLevel logLevel, String tag, String message) { + long msec; + + msec = System.currentTimeMillis(); + String outMessage = String.format("%02d:%02d %c/%s: %s\n", + (msec / 60000) % 60, (msec / 1000) % 60, + logLevel.getPriorityLetter(), tag, message); + System.out.print(outMessage); + } + +} + + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java new file mode 100644 index 0000000..79eb5bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java @@ -0,0 +1,780 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.NotYetBoundException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Monitor open connections. + */ +final class MonitorThread extends Thread { + + // For broadcasts to message handlers + //private static final int CLIENT_CONNECTED = 1; + + private static final int CLIENT_READY = 2; + + private static final int CLIENT_DISCONNECTED = 3; + + private volatile boolean mQuit = false; + + // List of clients we're paying attention to + private ArrayList<Client> mClientList; + + // The almighty mux + private Selector mSelector; + + // Map chunk types to handlers + private HashMap<Integer, ChunkHandler> mHandlerMap; + + // port for "debug selected" + private ServerSocketChannel mDebugSelectedChan; + + private int mNewDebugSelectedPort; + + private int mDebugSelectedPort = -1; + + /** + * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port. + */ + private Client mSelectedClient = null; + + // singleton + private static MonitorThread mInstance; + + /** + * Generic constructor. + */ + private MonitorThread() { + super("Monitor"); + mClientList = new ArrayList<Client>(); + mHandlerMap = new HashMap<Integer, ChunkHandler>(); + + mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort(); + } + + /** + * Creates and return the singleton instance of the client monitor thread. + */ + static MonitorThread createInstance() { + return mInstance = new MonitorThread(); + } + + /** + * Get singleton instance of the client monitor thread. + */ + static MonitorThread getInstance() { + return mInstance; + } + + + /** + * Sets or changes the port number for "debug selected". + */ + synchronized void setDebugSelectedPort(int port) throws IllegalStateException { + if (mInstance == null) { + return; + } + + if (AndroidDebugBridge.getClientSupport() == false) { + return; + } + + if (mDebugSelectedChan != null) { + Log.d("ddms", "Changing debug-selected port to " + port); + mNewDebugSelectedPort = port; + wakeup(); + } else { + // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically + // opened on the first run loop. + mNewDebugSelectedPort = port; + } + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + synchronized void setSelectedClient(Client selectedClient) { + if (mInstance == null) { + return; + } + + if (mSelectedClient != selectedClient) { + Client oldClient = mSelectedClient; + mSelectedClient = selectedClient; + + if (oldClient != null) { + oldClient.update(Client.CHANGE_PORT); + } + + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + } + } + + /** + * Returns the client accepting debugger connection on the custom "Selected debug port". + */ + Client getSelectedClient() { + return mSelectedClient; + } + + + /** + * Returns "true" if we want to retry connections to clients if we get a bad + * JDWP handshake back, "false" if we want to just mark them as bad and + * leave them alone. + */ + boolean getRetryOnBadHandshake() { + return true; // TODO? make configurable + } + + /** + * Get an array of known clients. + */ + Client[] getClients() { + synchronized (mClientList) { + return mClientList.toArray(new Client[0]); + } + } + + /** + * Register "handler" as the handler for type "type". + */ + synchronized void registerChunkHandler(int type, ChunkHandler handler) { + if (mInstance == null) { + return; + } + + synchronized (mHandlerMap) { + if (mHandlerMap.get(type) == null) { + mHandlerMap.put(type, handler); + } + } + } + + /** + * Watch for activity from clients and debuggers. + */ + @Override + public void run() { + Log.d("ddms", "Monitor is up"); + + // create a selector + try { + mSelector = Selector.open(); + } catch (IOException ioe) { + Log.logAndDisplay(LogLevel.ERROR, "ddms", + "Failed to initialize Monitor Thread: " + ioe.getMessage()); + return; + } + + while (!mQuit) { + + try { + /* + * sync with new registrations: we wait until addClient is done before going through + * and doing mSelector.select() again. + * @see {@link #addClient(Client)} + */ + synchronized (mClientList) { + } + + // (re-)open the "debug selected" port, if it's not opened yet or + // if the port changed. + try { + if (AndroidDebugBridge.getClientSupport()) { + if ((mDebugSelectedChan == null || + mNewDebugSelectedPort != mDebugSelectedPort) && + mNewDebugSelectedPort != -1) { + if (reopenDebugSelectedPort()) { + mDebugSelectedPort = mNewDebugSelectedPort; + } + } + } + } catch (IOException ioe) { + Log.e("ddms", + "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort); + Log.e("ddms", ioe); + mNewDebugSelectedPort = mDebugSelectedPort; // no retry + } + + int count; + try { + count = mSelector.select(); + } catch (IOException ioe) { + ioe.printStackTrace(); + continue; + } catch (CancelledKeyException cke) { + continue; + } + + if (count == 0) { + // somebody called wakeup() ? + // Log.i("ddms", "selector looping"); + continue; + } + + Set<SelectionKey> keys = mSelector.selectedKeys(); + Iterator<SelectionKey> iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + try { + if (key.attachment() instanceof Client) { + processClientActivity(key); + } + else if (key.attachment() instanceof Debugger) { + processDebuggerActivity(key); + } + else if (key.attachment() instanceof MonitorThread) { + processDebugSelectedActivity(key); + } + else { + Log.e("ddms", "unknown activity key"); + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception during activity from Selector."); + Log.e("ddms", e); + } + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception MonitorThread.run()"); + Log.e("ddms", e); + } + } + } + + + /** + * Returns the port on which the selected client listen for debugger + */ + int getDebugSelectedPort() { + return mDebugSelectedPort; + } + + /* + * Something happened. Figure out what. + */ + private void processClientActivity(SelectionKey key) { + Client client = (Client)key.attachment(); + + try { + if (key.isReadable() == false || key.isValid() == false) { + Log.d("ddms", "Invalid key from " + client + ". Dropping client."); + dropClient(client, true /* notify */); + return; + } + + client.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = client.getJdwpPacket(); + while (packet != null) { + if (packet.isDdmPacket()) { + // unsolicited DDM request - hand it off + assert !packet.isReply(); + callHandler(client, packet, null); + packet.consume(); + } else if (packet.isReply() + && client.isResponseToUs(packet.getId()) != null) { + // reply to earlier DDM request + ChunkHandler handler = client + .isResponseToUs(packet.getId()); + if (packet.isError()) + client.packetFailed(packet); + else if (packet.isEmpty()) + Log.d("ddms", "Got empty reply for 0x" + + Integer.toHexString(packet.getId()) + + " from " + client); + else + callHandler(client, packet, handler); + packet.consume(); + client.removeRequestId(packet.getId()); + } else { + Log.v("ddms", "Forwarding client " + + (packet.isReply() ? "reply" : "event") + " 0x" + + Integer.toHexString(packet.getId()) + " to " + + client.getDebugger()); + client.forwardPacketToDebugger(packet); + } + + // find next + packet = client.getJdwpPacket(); + } + } catch (CancelledKeyException e) { + // key was canceled probably due to a disconnected client before we could + // read stuff coming from the client, so we drop it. + dropClient(client, true /* notify */); + } catch (IOException ex) { + // something closed down, no need to print anything. The client is simply dropped. + dropClient(client, true /* notify */); + } catch (Exception ex) { + Log.e("ddms", ex); + + /* close the client; automatically un-registers from selector */ + dropClient(client, true /* notify */); + + if (ex instanceof BufferOverflowException) { + Log.w("ddms", + "Client data packet exceeded maximum buffer size " + + client); + } else { + // don't know what this is, display it + Log.e("ddms", ex); + } + } + } + + /* + * Process an incoming DDM packet. If this is a reply to an earlier request, + * "handler" will be set to the handler responsible for the original + * request. The spec allows a JDWP message to include multiple DDM chunks. + */ + private void callHandler(Client client, JdwpPacket packet, + ChunkHandler handler) { + + // on first DDM packet received, broadcast a "ready" message + if (!client.ddmSeen()) + broadcast(CLIENT_READY, client); + + ByteBuffer buf = packet.getPayload(); + int type, length; + boolean reply = true; + + type = buf.getInt(); + length = buf.getInt(); + + if (handler == null) { + // not a reply, figure out who wants it + synchronized (mHandlerMap) { + handler = mHandlerMap.get(type); + reply = false; + } + } + + if (handler == null) { + Log.w("ddms", "Received unsupported chunk type " + + ChunkHandler.name(type) + " (len=" + length + ")"); + } else { + Log.d("ddms", "Calling handler for " + ChunkHandler.name(type) + + " [" + handler + "] (len=" + length + ")"); + ByteBuffer ibuf = buf.slice(); + ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O + roBuf.order(ChunkHandler.CHUNK_ORDER); + // do the handling of the chunk synchronized on the client list + // to be sure there's no concurrency issue when we look for HOME + // in hasApp() + synchronized (mClientList) { + handler.handleChunk(client, type, roBuf, reply, packet.getId()); + } + } + } + + /** + * Drops a client from the monitor. + * <p/>This will lock the {@link Client} list of the {@link Device} running <var>client</var>. + * @param client + * @param notify + */ + synchronized void dropClient(Client client, boolean notify) { + if (mInstance == null) { + return; + } + + synchronized (mClientList) { + if (mClientList.remove(client) == false) { + return; + } + } + client.close(notify); + broadcast(CLIENT_DISCONNECTED, client); + + /* + * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0 + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504 + */ + wakeup(); + } + + /* + * Process activity from one of the debugger sockets. This could be a new + * connection or a data packet. + */ + private void processDebuggerActivity(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + if (key.isAcceptable()) { + try { + acceptNewDebugger(dbg, null); + } catch (IOException ioe) { + Log.w("ddms", "debugger accept() failed"); + ioe.printStackTrace(); + } + } else if (key.isReadable()) { + processDebuggerData(key); + } else { + Log.d("ddm-debugger", "key in unknown state"); + } + } catch (CancelledKeyException cke) { + // key has been cancelled we can ignore that. + } + } + + /* + * Accept a new connection from a debugger. If successful, register it with + * the Selector. + */ + private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) + throws IOException { + + synchronized (mClientList) { + SocketChannel chan; + + if (acceptChan == null) + chan = dbg.accept(); + else + chan = dbg.accept(acceptChan); + + if (chan != null) { + chan.socket().setTcpNoDelay(true); + + wakeup(); + + try { + chan.register(mSelector, SelectionKey.OP_READ, dbg); + } catch (IOException ioe) { + // failed, drop the connection + dbg.closeData(); + throw ioe; + } catch (RuntimeException re) { + // failed, drop the connection + dbg.closeData(); + throw re; + } + } else { + Log.i("ddms", "ignoring duplicate debugger"); + // new connection already closed + } + } + } + + /* + * We have incoming data from the debugger. Forward it to the client. + */ + private void processDebuggerData(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + /* + * Read pending data. + */ + dbg.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = dbg.getJdwpPacket(); + while (packet != null) { + Log.v("ddms", "Forwarding dbg req 0x" + + Integer.toHexString(packet.getId()) + " to " + + dbg.getClient()); + + dbg.forwardPacketToClient(packet); + + packet = dbg.getJdwpPacket(); + } + } catch (IOException ioe) { + /* + * Close data connection; automatically un-registers dbg from + * selector. The failure could be caused by the debugger going away, + * or by the client going away and failing to accept our data. + * Either way, the debugger connection does not need to exist any + * longer. We also need to recycle the connection to the client, so + * that the VM sees the debugger disconnect. For a DDM-aware client + * this won't be necessary, and we can just send a "debugger + * disconnected" message. + */ + Log.i("ddms", "Closing connection to debugger " + dbg); + dbg.closeData(); + Client client = dbg.getClient(); + if (client.isDdmAware()) { + // TODO: soft-disconnect DDM-aware clients + Log.i("ddms", " (recycling client connection as well)"); + + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDevice().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } else { + Log.i("ddms", " (recycling client connection as well)"); + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDevice().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } + } + } + + /* + * Tell the thread that something has changed. + */ + private void wakeup() { + mSelector.wakeup(); + } + + /** + * Tell the thread to stop. Called from UI thread. + */ + synchronized void quit() { + mQuit = true; + wakeup(); + Log.d("ddms", "Waiting for Monitor thread"); + try { + this.join(); + // since we're quitting, lets drop all the client and disconnect + // the DebugSelectedPort + synchronized (mClientList) { + for (Client c : mClientList) { + c.close(false /* notify */); + broadcast(CLIENT_DISCONNECTED, c); + } + mClientList.clear(); + } + + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + mDebugSelectedChan.socket().close(); + mDebugSelectedChan = null; + } + mSelector.close(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + mInstance = null; + } + + /** + * Add a new Client to the list of things we monitor. Also adds the client's + * channel and the client's debugger listener to the selection list. This + * should only be called from one thread (the VMWatcherThread) to avoid a + * race between "alreadyOpen" and Client creation. + */ + synchronized void addClient(Client client) { + if (mInstance == null) { + return; + } + + Log.d("ddms", "Adding new client " + client); + + synchronized (mClientList) { + mClientList.add(client); + + /* + * Register the Client's socket channel with the selector. We attach + * the Client to the SelectionKey. If you try to register a new + * channel with the Selector while it is waiting for I/O, you will + * block. The solution is to call wakeup() and then hold a lock to + * ensure that the registration happens before the Selector goes + * back to sleep. + */ + try { + wakeup(); + + client.register(mSelector); + + Debugger dbg = client.getDebugger(); + if (dbg != null) { + dbg.registerListener(mSelector); + } + } catch (IOException ioe) { + // not really expecting this to happen + ioe.printStackTrace(); + } + } + } + + /* + * Broadcast an event to all message handlers. + */ + private void broadcast(int event, Client client) { + Log.d("ddms", "broadcast " + event + ": " + client); + + /* + * The handler objects appear once in mHandlerMap for each message they + * handle. We want to notify them once each, so we convert the HashMap + * to a HashSet before we iterate. + */ + HashSet<ChunkHandler> set; + synchronized (mHandlerMap) { + Collection<ChunkHandler> values = mHandlerMap.values(); + set = new HashSet<ChunkHandler>(values); + } + + Iterator<ChunkHandler> iter = set.iterator(); + while (iter.hasNext()) { + ChunkHandler handler = iter.next(); + switch (event) { + case CLIENT_READY: + try { + handler.clientReady(client); + } catch (IOException ioe) { + // Something failed with the client. It should + // fall out of the list the next time we try to + // do something with it, so we discard the + // exception here and assume cleanup will happen + // later. May need to propagate farther. The + // trouble is that not all values for "event" may + // actually throw an exception. + Log.w("ddms", + "Got exception while broadcasting 'ready'"); + return; + } + break; + case CLIENT_DISCONNECTED: + handler.clientDisconnected(client); + break; + default: + throw new UnsupportedOperationException(); + } + } + + } + + /** + * Opens (or reopens) the "debug selected" port and listen for connections. + * @return true if the port was opened successfully. + * @throws IOException + */ + private boolean reopenDebugSelectedPort() throws IOException { + + Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort); + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + } + + mDebugSelectedChan = ServerSocketChannel.open(); + mDebugSelectedChan.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), //$NON-NLS-1$ + mNewDebugSelectedPort); + mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR + + try { + mDebugSelectedChan.socket().bind(addr); + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + + mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this); + + return true; + } catch (java.net.BindException e) { + displayDebugSelectedBindError(mNewDebugSelectedPort); + + // do not attempt to reopen it. + mDebugSelectedChan = null; + mNewDebugSelectedPort = -1; + + return false; + } + } + + /* + * We have some activity on the "debug selected" port. Handle it. + */ + private void processDebugSelectedActivity(SelectionKey key) { + assert key.isAcceptable(); + + ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel(); + + /* + * Find the debugger associated with the currently-selected client. + */ + if (mSelectedClient != null) { + Debugger dbg = mSelectedClient.getDebugger(); + + if (dbg != null) { + Log.i("ddms", "Accepting connection on 'debug selected' port"); + try { + acceptNewDebugger(dbg, acceptChan); + } catch (IOException ioe) { + // client should be gone, keep going + } + + return; + } + } + + Log.w("ddms", + "Connection on 'debug selected' port, but none selected"); + try { + SocketChannel chan = acceptChan.accept(); + chan.close(); + } catch (IOException ioe) { + // not expected; client should be gone, keep going + } catch (NotYetBoundException e) { + displayDebugSelectedBindError(mDebugSelectedPort); + } + } + + private void displayDebugSelectedBindError(int port) { + String message = String.format( + "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.", + port); + + Log.logAndDisplay(LogLevel.ERROR, "ddms", message); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java new file mode 100644 index 0000000..24dbb05 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +/** + * Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the + * socket, and convert it into {@link String} objects. + * <p/>Additionally, it splits the string by lines. + * <p/>Classes extending it must implement {@link #processNewLines(String[])} which receives + * new parsed lines as they become available. + */ +public abstract class MultiLineReceiver implements IShellOutputReceiver { + + private boolean mTrimLines = true; + + /** unfinished message line, stored for next packet */ + private String mUnfinishedLine = null; + + private final ArrayList<String> mArray = new ArrayList<String>(); + + /** + * Set the trim lines flag. + * @param trim hether the lines are trimmed, or not. + */ + public void setTrimLine(boolean trim) { + mTrimLines = trim; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput( + * byte[], int, int) + */ + public final void addOutput(byte[] data, int offset, int length) { + if (isCancelled() == false) { + String s = null; + try { + s = new String(data, offset, length, "ISO-8859-1"); //$NON-NLS-1$ + } catch (UnsupportedEncodingException e) { + // normal encoding didn't work, try the default one + s = new String(data, offset,length); + } + + // ok we've got a string + if (s != null) { + // if we had an unfinished line we add it. + if (mUnfinishedLine != null) { + s = mUnfinishedLine + s; + mUnfinishedLine = null; + } + + // now we split the lines + mArray.clear(); + int start = 0; + do { + int index = s.indexOf("\r\n", start); //$NON-NLS-1$ + + // if \r\n was not found, this is an unfinished line + // and we store it to be processed for the next packet + if (index == -1) { + mUnfinishedLine = s.substring(start); + break; + } + + // so we found a \r\n; + // extract the line + String line = s.substring(start, index); + if (mTrimLines) { + line = line.trim(); + } + mArray.add(line); + + // move start to after the \r\n we found + start = index + 2; + } while (true); + + if (mArray.size() > 0) { + // at this point we've split all the lines. + // make the array + String[] lines = mArray.toArray(new String[mArray.size()]); + + // send it for final processing + processNewLines(lines); + } + } + } + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + public final void flush() { + if (mUnfinishedLine != null) { + processNewLines(new String[] { mUnfinishedLine }); + } + + done(); + } + + /** + * Terminates the process. This is called after the last lines have been through + * {@link #processNewLines(String[])}. + */ + public void done() { + // do nothing. + } + + /** + * Called when new lines are being received by the remote process. + * <p/>It is guaranteed that the lines are complete when they are given to this method. + * @param lines The array containing the new lines. + */ + public abstract void processNewLines(String[] lines); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java new file mode 100644 index 0000000..956b004 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Stores native allocation information. + * <p/>Contains number of allocations, their size and the stack trace. + * <p/>Note: the ddmlib does not resolve the stack trace automatically. While this class provides + * storage for resolved stack trace, this is merely for convenience. + */ +public final class NativeAllocationInfo { + /* constants for flag bits */ + private static final int FLAG_ZYGOTE_CHILD = (1<<31); + private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD); + + /** + * list of alloc functions that are filtered out when attempting to display + * a relevant method responsible for an allocation + */ + private static ArrayList<String> sAllocFunctionFilter; + static { + sAllocFunctionFilter = new ArrayList<String>(); + sAllocFunctionFilter.add("malloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("calloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("realloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("get_backtrace"); //$NON-NLS-1$ + sAllocFunctionFilter.add("get_hash"); //$NON-NLS-1$ + sAllocFunctionFilter.add("??"); //$NON-NLS-1$ + sAllocFunctionFilter.add("internal_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("operator new"); //$NON-NLS-1$ + sAllocFunctionFilter.add("leak_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("chk_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("chk_memalign"); //$NON-NLS-1$ + sAllocFunctionFilter.add("Malloc"); //$NON-NLS-1$ + } + + private final int mSize; + + private final boolean mIsZygoteChild; + + private final int mAllocations; + + private final ArrayList<Long> mStackCallAddresses = new ArrayList<Long>(); + + private ArrayList<NativeStackCallInfo> mResolvedStackCall = null; + + private boolean mIsStackCallResolved = false; + + /** + * Constructs a new {@link NativeAllocationInfo}. + * @param size The size of the allocations. + * @param allocations the allocation count + */ + NativeAllocationInfo(int size, int allocations) { + this.mSize = size & ~FLAG_MASK; + this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0); + this.mAllocations = allocations; + } + + /** + * Adds a stack call address for this allocation. + * @param address The address to add. + */ + void addStackCallAddress(long address) { + mStackCallAddresses.add(address); + } + + /** + * Returns the total size of this allocation. + */ + public int getSize() { + return mSize; + } + + /** + * Returns whether the allocation happened in a child of the zygote + * process. + */ + public boolean isZygoteChild() { + return mIsZygoteChild; + } + + /** + * Returns the allocation count. + */ + public int getAllocationCount() { + return mAllocations; + } + + /** + * Returns whether the stack call addresses have been resolved into + * {@link NativeStackCallInfo} objects. + */ + public boolean isStackCallResolved() { + return mIsStackCallResolved; + } + + /** + * Returns the stack call of this allocation as raw addresses. + * @return the list of addresses where the allocation happened. + */ + public Long[] getStackCallAddresses() { + return mStackCallAddresses.toArray(new Long[mStackCallAddresses.size()]); + } + + /** + * Sets the resolved stack call for this allocation. + * <p/> + * If <code>resolvedStackCall</code> is non <code>null</code> then + * {@link #isStackCallResolved()} will return <code>true</code> after this call. + * @param resolvedStackCall The list of {@link NativeStackCallInfo}. + */ + public synchronized void setResolvedStackCall(List<NativeStackCallInfo> resolvedStackCall) { + if (mResolvedStackCall == null) { + mResolvedStackCall = new ArrayList<NativeStackCallInfo>(); + } else { + mResolvedStackCall.clear(); + } + mResolvedStackCall.addAll(resolvedStackCall); + mIsStackCallResolved = mResolvedStackCall.size() != 0; + } + + /** + * Returns the resolved stack call. + * @return An array of {@link NativeStackCallInfo} or <code>null</code> if the stack call + * was not resolved. + * @see #setResolvedStackCall(ArrayList) + * @see #isStackCallResolved() + */ + public synchronized NativeStackCallInfo[] getResolvedStackCall() { + if (mIsStackCallResolved) { + return mResolvedStackCall.toArray(new NativeStackCallInfo[mResolvedStackCall.size()]); + } + + return null; + } + + /** + * Indicates whether some other object is "equal to" this one. + * @param obj the reference object with which to compare. + * @return <code>true</code> if this object is equal to the obj argument; + * <code>false</code> otherwise. + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof NativeAllocationInfo) { + NativeAllocationInfo mi = (NativeAllocationInfo)obj; + // quick compare of size, alloc, and stackcall size + if (mSize != mi.mSize || mAllocations != mi.mAllocations || + mStackCallAddresses.size() != mi.mStackCallAddresses.size()) { + return false; + } + // compare the stack addresses + int count = mStackCallAddresses.size(); + for (int i = 0 ; i < count ; i++) { + long a = mStackCallAddresses.get(i); + long b = mi.mStackCallAddresses.get(i); + if (a != b) { + return false; + } + } + + return true; + } + return false; + } + + /** + * Returns a string representation of the object. + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("Allocations: "); + buffer.append(mAllocations); + buffer.append("\n"); //$NON-NLS-1$ + + buffer.append("Size: "); + buffer.append(mSize); + buffer.append("\n"); //$NON-NLS-1$ + + buffer.append("Total Size: "); + buffer.append(mSize * mAllocations); + buffer.append("\n"); //$NON-NLS-1$ + + Iterator<Long> addrIterator = mStackCallAddresses.iterator(); + Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator(); + + while (sourceIterator.hasNext()) { + long addr = addrIterator.next(); + NativeStackCallInfo source = sourceIterator.next(); + if (addr == 0) + continue; + + if (source.getLineNumber() != -1) { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr, + source.getLibraryName(), source.getMethodName(), + source.getSourceFile(), source.getLineNumber())); + } else { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr, + source.getLibraryName(), source.getMethodName(), source.getSourceFile())); + } + } + + return buffer.toString(); + } + + /** + * Returns the first {@link NativeStackCallInfo} that is relevant. + * <p/> + * A relevant <code>NativeStackCallInfo</code> is a stack call that is not deep in the + * lower level of the libc, but the actual method that performed the allocation. + * @return a <code>NativeStackCallInfo</code> or <code>null</code> if the stack call has not + * been processed from the raw addresses. + * @see #setResolvedStackCall(ArrayList) + * @see #isStackCallResolved() + */ + public synchronized NativeStackCallInfo getRelevantStackCallInfo() { + if (mIsStackCallResolved && mResolvedStackCall != null) { + Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator(); + Iterator<Long> addrIterator = mStackCallAddresses.iterator(); + + while (sourceIterator.hasNext() && addrIterator.hasNext()) { + long addr = addrIterator.next(); + NativeStackCallInfo info = sourceIterator.next(); + if (addr != 0 && info != null) { + if (isRelevant(info.getMethodName())) { + return info; + } + } + } + + // couldnt find a relevant one, so we'll return the first one if it + // exists. + if (mResolvedStackCall.size() > 0) + return mResolvedStackCall.get(0); + } + + return null; + } + + /** + * Returns true if the method name is relevant. + * @param methodName the method name to test. + */ + private boolean isRelevant(String methodName) { + for (String filter : sAllocFunctionFilter) { + if (methodName.contains(filter)) { + return false; + } + } + + return true; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java new file mode 100644 index 0000000..5a26317 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Memory address to library mapping for native libraries. + * <p/> + * Each instance represents a single native library and its start and end memory addresses. + */ +public final class NativeLibraryMapInfo { + private long mStartAddr; + private long mEndAddr; + + private String mLibrary; + + /** + * Constructs a new native library map info. + * @param startAddr The start address of the library. + * @param endAddr The end address of the library. + * @param library The name of the library. + */ + NativeLibraryMapInfo(long startAddr, long endAddr, String library) { + this.mStartAddr = startAddr; + this.mEndAddr = endAddr; + this.mLibrary = library; + } + + /** + * Returns the name of the library. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the start address of the library. + */ + public long getStartAddress() { + return mStartAddr; + } + + /** + * Returns the end address of the library. + */ + public long getEndAddress() { + return mEndAddr; + } + + /** + * Returns whether the specified address is inside the library. + * @param address The address to test. + * @return <code>true</code> if the address is between the start and end address of the library. + * @see #getStartAddress() + * @see #getEndAddress() + */ + public boolean isWithinLibrary(long address) { + return address >= mStartAddr && address <= mEndAddr; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java new file mode 100644 index 0000000..e54818d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a stack call. This is used to return all of the call + * information as one object. + */ +public final class NativeStackCallInfo { + private final static Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$"); + + /** name of the library */ + private String mLibrary; + + /** name of the method */ + private String mMethod; + + /** + * name of the source file + line number in the format<br> + * <sourcefile>:<linenumber> + */ + private String mSourceFile; + + private int mLineNumber = -1; + + /** + * Basic constructor with library, method, and sourcefile information + * + * @param lib The name of the library + * @param method the name of the method + * @param sourceFile the name of the source file and the line number + * as "[sourcefile]:[fileNumber]" + */ + public NativeStackCallInfo(String lib, String method, String sourceFile) { + mLibrary = lib; + mMethod = method; + + Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile); + if (m.matches()) { + mSourceFile = m.group(1); + try { + mLineNumber = Integer.parseInt(m.group(2)); + } catch (NumberFormatException e) { + // do nothing, the line number will stay at -1 + } + } else { + mSourceFile = sourceFile; + } + } + + /** + * Returns the name of the library name. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the name of the method. + */ + public String getMethodName() { + return mMethod; + } + + /** + * Returns the name of the source file. + */ + public String getSourceFile() { + return mSourceFile; + } + + /** + * Returns the line number, or -1 if unknown. + */ + public int getLineNumber() { + return mLineNumber; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java new file mode 100644 index 0000000..d2b5a1e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Implementation of {@link IShellOutputReceiver} that does nothing. + * <p/>This can be used to execute a remote shell command when the output is not needed. + */ +public final class NullOutputReceiver implements IShellOutputReceiver { + + private static NullOutputReceiver sReceiver = new NullOutputReceiver(); + + public static IShellOutputReceiver getReceiver() { + return sReceiver; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int) + */ + public void addOutput(byte[] data, int offset, int length) { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + public void flush() { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return false; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java new file mode 100644 index 0000000..610cb59 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Data representing an image taken from a device frame buffer. + */ +public final class RawImage { + /** + * bit-per-pixel value. + */ + public int bpp; + public int size; + public int width; + public int height; + + public byte[] data; +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java new file mode 100644 index 0000000..44df000 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java @@ -0,0 +1,949 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.utils.ArrayHelper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; + +/** + * Sync service class to push/pull to/from devices/emulators, through the debug bridge. + * <p/> + * To get a {@link SyncService} object, use {@link Device#getSyncService()}. + */ +public final class SyncService { + + private final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' }; + private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' }; + private final static byte[] ID_STAT = { 'S', 'T', 'A', 'T' }; + private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' }; + private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' }; + private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' }; + private final static byte[] ID_SEND = { 'S', 'E', 'N', 'D' }; +// private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' }; +// private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' }; + + private final static NullSyncProgresMonitor sNullSyncProgressMonitor = + new NullSyncProgresMonitor(); + + private final static int S_ISOCK = 0xC000; // type: symbolic link + private final static int S_IFLNK = 0xA000; // type: symbolic link + private final static int S_IFREG = 0x8000; // type: regular file + private final static int S_IFBLK = 0x6000; // type: block device + private final static int S_IFDIR = 0x4000; // type: directory + private final static int S_IFCHR = 0x2000; // type: character device + private final static int S_IFIFO = 0x1000; // type: fifo +/* + private final static int S_ISUID = 0x0800; // set-uid bit + private final static int S_ISGID = 0x0400; // set-gid bit + private final static int S_ISVTX = 0x0200; // sticky bit + private final static int S_IRWXU = 0x01C0; // user permissions + private final static int S_IRUSR = 0x0100; // user: read + private final static int S_IWUSR = 0x0080; // user: write + private final static int S_IXUSR = 0x0040; // user: execute + private final static int S_IRWXG = 0x0038; // group permissions + private final static int S_IRGRP = 0x0020; // group: read + private final static int S_IWGRP = 0x0010; // group: write + private final static int S_IXGRP = 0x0008; // group: execute + private final static int S_IRWXO = 0x0007; // other permissions + private final static int S_IROTH = 0x0004; // other: read + private final static int S_IWOTH = 0x0002; // other: write + private final static int S_IXOTH = 0x0001; // other: execute +*/ + + private final static int SYNC_DATA_MAX = 64*1024; + private final static int REMOTE_PATH_MAX_LENGTH = 1024; + + /** Result code for transfer success. */ + public static final int RESULT_OK = 0; + /** Result code for canceled transfer */ + public static final int RESULT_CANCELED = 1; + /** Result code for unknown error */ + public static final int RESULT_UNKNOWN_ERROR = 2; + /** Result code for network connection error */ + public static final int RESULT_CONNECTION_ERROR = 3; + /** Result code for unknown remote object during a pull */ + public static final int RESULT_NO_REMOTE_OBJECT = 4; + /** Result code when attempting to pull multiple files into a file */ + public static final int RESULT_TARGET_IS_FILE = 5; + /** Result code when attempting to pull multiple into a directory that does not exist. */ + public static final int RESULT_NO_DIR_TARGET = 6; + /** Result code for wrong encoding on the remote path. */ + public static final int RESULT_REMOTE_PATH_ENCODING = 7; + /** Result code for remote path that is too long. */ + public static final int RESULT_REMOTE_PATH_LENGTH = 8; + /** Result code for error while writing local file. */ + public static final int RESULT_FILE_WRITE_ERROR = 9; + /** Result code for error while reading local file. */ + public static final int RESULT_FILE_READ_ERROR = 10; + /** Result code for attempting to push a file that does not exist. */ + public static final int RESULT_NO_LOCAL_FILE = 11; + /** Result code for attempting to push a directory. */ + public static final int RESULT_LOCAL_IS_DIRECTORY = 12; + /** Result code for when the target path of a multi file push is a file. */ + public static final int RESULT_REMOTE_IS_FILE = 13; + /** Result code for receiving too much data from the remove device at once */ + public static final int RESULT_BUFFER_OVERRUN = 14; + + /** + * A file transfer result. + * <p/> + * This contains a code, and an optional string + */ + public static class SyncResult { + private int mCode; + private String mMessage; + SyncResult(int code, String message) { + mCode = code; + mMessage = message; + } + + SyncResult(int code, Exception e) { + this(code, e.getMessage()); + } + + SyncResult(int code) { + this(code, errorCodeToString(code)); + } + + public int getCode() { + return mCode; + } + + public String getMessage() { + return mMessage; + } + } + + /** + * Classes which implement this interface provide methods that deal + * with displaying transfer progress. + */ + public interface ISyncProgressMonitor { + /** + * Sent when the transfer starts + * @param totalWork the total amount of work. + */ + public void start(int totalWork); + /** + * Sent when the transfer is finished or interrupted. + */ + public void stop(); + /** + * Sent to query for possible cancellation. + * @return true if the transfer should be stopped. + */ + public boolean isCanceled(); + /** + * Sent when a sub task is started. + * @param name the name of the sub task. + */ + public void startSubTask(String name); + /** + * Sent when some progress have been made. + * @param work the amount of work done. + */ + public void advance(int work); + } + + /** + * A Sync progress monitor that does nothing + */ + private static class NullSyncProgresMonitor implements ISyncProgressMonitor { + public void advance(int work) { + } + public boolean isCanceled() { + return false; + } + + public void start(int totalWork) { + } + public void startSubTask(String name) { + } + public void stop() { + } + } + + private InetSocketAddress mAddress; + private Device mDevice; + private SocketChannel mChannel; + + /** + * Buffer used to send data. Allocated when needed and reused afterward. + */ + private byte[] mBuffer; + + /** + * Creates a Sync service object. + * @param address The address to connect to + * @param device the {@link Device} that the service connects to. + */ + SyncService(InetSocketAddress address, Device device) { + mAddress = address; + mDevice = device; + } + + /** + * Opens the sync connection. This must be called before any calls to push[File] / pull[File]. + * @return true if the connection opened, false otherwise. + */ + boolean openSync() { + try { + mChannel = SocketChannel.open(mAddress); + mChannel.configureBlocking(false); + + // target a specific device + AdbHelper.setDevice(mChannel, mDevice); + + byte[] request = AdbHelper.formAdbRequest("sync:"); // $NON-NLS-1$ + AdbHelper.write(mChannel, request, -1, AdbHelper.STD_TIMEOUT); + + AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */); + + if (!resp.ioSuccess || !resp.okay) { + Log.w("ddms", + "Got timeout or unhappy response from ADB sync req: " + + resp.message); + mChannel.close(); + mChannel = null; + return false; + } + } catch (IOException e) { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e1) { + // we do nothing, since we'll return false just below + } + mChannel = null; + return false; + } + } + return true; + } + + /** + * Closes the connection. + */ + public void close() { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e) { + // nothing to be done really... + } + mChannel = null; + } + } + + /** + * Returns a sync progress monitor that does nothing. This allows background tasks that don't + * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}. + * <p/>This object can be reused multiple times and can be used by concurrent threads. + */ + public static ISyncProgressMonitor getNullProgressMonitor() { + return sNullSyncProgressMonitor; + } + + /** + * Converts an error code into a non-localized string + * @param code the error code; + */ + private static String errorCodeToString(int code) { + switch (code) { + case RESULT_OK: + return "Success."; + case RESULT_CANCELED: + return "Tranfert canceled by the user."; + case RESULT_UNKNOWN_ERROR: + return "Unknown Error."; + case RESULT_CONNECTION_ERROR: + return "Adb Connection Error."; + case RESULT_NO_REMOTE_OBJECT: + return "Remote object doesn't exist!"; + case RESULT_TARGET_IS_FILE: + return "Target object is a file."; + case RESULT_NO_DIR_TARGET: + return "Target directory doesn't exist."; + case RESULT_REMOTE_PATH_ENCODING: + return "Remote Path encoding is not supported."; + case RESULT_REMOTE_PATH_LENGTH: + return "Remove path is too long."; + case RESULT_FILE_WRITE_ERROR: + return "Writing local file failed!"; + case RESULT_FILE_READ_ERROR: + return "Reading local file failed!"; + case RESULT_NO_LOCAL_FILE: + return "Local file doesn't exist."; + case RESULT_LOCAL_IS_DIRECTORY: + return "Local path is a directory."; + case RESULT_REMOTE_IS_FILE: + return "Remote path is a file."; + case RESULT_BUFFER_OVERRUN: + return "Receiving too much data."; + } + + throw new RuntimeException(); + } + + /** + * Pulls file(s) or folder(s). + * @param entries the remote item(s) to pull + * @param localPath The local destination. If the entries count is > 1 or + * if the unique entry is a folder, this should be a folder. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) { + + // first we check the destination is a directory and exists + File f = new File(localPath); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_DIR_TARGET); + } + if (f.isDirectory() == false) { + return new SyncResult(RESULT_TARGET_IS_FILE); + } + + // get a FileListingService object + FileListingService fls = new FileListingService(mDevice); + + // compute the number of file to move + int total = getTotalRemoteFileSize(entries, fls); + + // start the monitor + monitor.start(total); + + SyncResult result = doPull(entries, localPath, fls, monitor); + + monitor.stop(); + + return result; + } + + /** + * Pulls a single file. + * @param remote the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(FileEntry remote, String localFilename, + ISyncProgressMonitor monitor) { + int total = remote.getSizeValue(); + monitor.start(total); + + SyncResult result = doPullFile(remote.getFullPath(), localFilename, monitor); + + monitor.stop(); + return result; + } + + /** + * Push several files. + * @param local An array of loca files to push + * @param remote the remote {@link FileEntry} representing a directory. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) { + if (remote.isDirectory() == false) { + return new SyncResult(RESULT_REMOTE_IS_FILE); + } + + // make a list of File from the list of String + ArrayList<File> files = new ArrayList<File>(); + for (String path : local) { + files.add(new File(path)); + } + + // get the total count of the bytes to transfer + File[] fileArray = files.toArray(new File[files.size()]); + int total = getTotalLocalFileSize(fileArray); + + monitor.start(total); + + SyncResult result = doPush(fileArray, remote.getFullPath(), monitor); + + monitor.stop(); + + return result; + } + + /** + * Push a single file. + * @param local the local filepath. + * @param remote The remote filepath. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) { + File f = new File(local); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + if (f.isDirectory()) { + return new SyncResult(RESULT_LOCAL_IS_DIRECTORY); + } + + monitor.start((int)f.length()); + + SyncResult result = doPushFile(local, remote, monitor); + + monitor.stop(); + + return result; + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * @param entries + * @param fls + * @return + */ + private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) { + int count = 0; + for (FileEntry e : entries) { + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + // get the children + FileEntry[] children = fls.getChildren(e, false, null); + count += getTotalRemoteFileSize(children, fls) + 1; + } else if (type == FileListingService.TYPE_FILE) { + count += e.getSizeValue(); + } + } + + return count; + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * This does not check for circular links. + * @param files + * @return + */ + private int getTotalLocalFileSize(File[] files) { + int count = 0; + + for (File f : files) { + if (f.exists()) { + if (f.isDirectory()) { + return getTotalLocalFileSize(f.listFiles()) + 1; + } else if (f.isFile()) { + count += f.length(); + } + } + } + + return count; + } + + /** + * Pulls multiple files/folders recursively. + * @param entries The list of entry to pull + * @param localPath the localpath to a directory + * @param fileListingService a FileListingService object to browse through remote directories. + * @param monitor the progress monitor. Must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPull(FileEntry[] entries, String localPath, + FileListingService fileListingService, + ISyncProgressMonitor monitor) { + + for (FileEntry e : entries) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // get type (we only pull directory and files for now) + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + + // make the directory + File d = new File(dest); + d.mkdir(); + + // then recursively call the content. Since we did a ls command + // to get the number of files, we can use the cache + FileEntry[] children = fileListingService.getChildren(e, true, null); + SyncResult result = doPull(children, dest, fileListingService, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + monitor.advance(1); + } else if (type == FileListingService.TYPE_FILE) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + SyncResult result = doPullFile(e.getFullPath(), dest, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * Pulls a remote file + * @param remotePath the remote file (length max is 1024) + * @param localPath the local destination + * @param monitor the monitor. The monitor must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPullFile(String remotePath, String localPath, + ISyncProgressMonitor monitor) { + byte[] msg = null; + byte[] pullResult = new byte[8]; + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + // create the full request message + msg = createFileReq(ID_RECV, remotePathContent); + + // and send it. + AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT); + + // read the result, in a byte array containing 2 ints + // (id, size) + AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT); + + // check we have the proper data back + if (checkResult(pullResult, ID_DATA) == false && + checkResult(pullResult, ID_DONE) == false) { + return new SyncResult(RESULT_CONNECTION_ERROR); + } + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // access the destination file + File f = new File(localPath); + + // create the stream to write in the file. We use a new try/catch block to differentiate + // between file and network io exceptions. + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + // the buffer to read the data + byte[] data = new byte[SYNC_DATA_MAX]; + + // loop to get data until we're done. + while (true) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // if we're done, we stop the loop + if (checkResult(pullResult, ID_DONE)) { + break; + } + if (checkResult(pullResult, ID_DATA) == false) { + // hmm there's an error + return new SyncResult(RESULT_CONNECTION_ERROR); + } + int length = ArrayHelper.swap32bitFromArray(pullResult, 4); + if (length > SYNC_DATA_MAX) { + // buffer overrun! + // error and exit + return new SyncResult(RESULT_BUFFER_OVERRUN); + } + + try { + // now read the length we received + AdbHelper.read(mChannel, data, length, AdbHelper.STD_TIMEOUT); + + // get the header for the next packet. + AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // write the content in the file + try { + fos.write(data, 0, length); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + monitor.advance(length); + } + + try { + fos.flush(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + return new SyncResult(RESULT_OK); + } + + + /** + * Push multiple files + * @param fileArray + * @param remotePath + * @param monitor + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) { + for (File f : fileArray) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + if (f.exists()) { + if (f.isDirectory()) { + // append the name of the directory to the remote path + String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(dest); + SyncResult result = doPush(f.listFiles(), dest, monitor); + + if (result.mCode != RESULT_OK) { + return result; + } + + monitor.advance(1); + } else if (f.isFile()) { + // append the name of the file to the remote path + String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(remoteFile); + SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * Push a single file + * @param localPath the local file to push + * @param remotePath the remote file (length max is 1024) + * @param monitor the monitor. The monitor must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPushFile(String localPath, String remotePath, + ISyncProgressMonitor monitor) { + FileInputStream fis = null; + byte[] msg; + + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + File f = new File(localPath); + + // this shouldn't happen but still... + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + // create the stream to read the file + fis = new FileInputStream(f); + + // create the header for the action + msg = createSendFileReq(ID_SEND, remotePathContent, 0644); + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + // and send it. We use a custom try/catch block to make the difference between + // file and network IO exceptions. + try { + AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // create the buffer used to read. + // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning. + if (mBuffer == null) { + mBuffer = new byte[SYNC_DATA_MAX + 8]; + } + System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length); + + // look while there is something to read + while (true) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // read up to SYNC_DATA_MAX + int readCount = 0; + try { + readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + if (readCount == -1) { + // we reached the end of the file + break; + } + + // now send the data to the device + // first write the amount read + ArrayHelper.swap32bitsToArray(readCount, mBuffer, 4); + + // now write it + try { + AdbHelper.write(mChannel, mBuffer, readCount+8, AdbHelper.STD_TIMEOUT); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // and advance the monitor + monitor.advance(readCount); + } + // close the local file + try { + fis.close(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + try { + // create the DONE message + long time = System.currentTimeMillis() / 1000; + msg = createReq(ID_DONE, (int)time); + + // and send it. + AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT); + + // read the result, in a byte array containing 2 ints + // (id, size) + byte[] result = new byte[8]; + AdbHelper.read(mChannel, result, -1 /* full length */, AdbHelper.STD_TIMEOUT); + + if (checkResult(result, ID_OKAY) == false) { + if (checkResult(result, ID_FAIL)) { + // read some error message... + int len = ArrayHelper.swap32bitFromArray(result, 4); + + AdbHelper.read(mChannel, mBuffer, len, AdbHelper.STD_TIMEOUT); + + // output the result? + String message = new String(mBuffer, 0, len); + Log.e("ddms", "transfer error: " + message); + return new SyncResult(RESULT_UNKNOWN_ERROR, message); + } + + return new SyncResult(RESULT_UNKNOWN_ERROR); + } + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + return new SyncResult(RESULT_OK); + } + + + /** + * Returns the mode of the remote file. + * @param path the remote file + * @return and Integer containing the mode if all went well or null + * otherwise + */ + private Integer readMode(String path) { + try { + // create the stat request message. + byte[] msg = createFileReq(ID_STAT, path); + + AdbHelper.write(mChannel, msg, -1 /* full length */, AdbHelper.STD_TIMEOUT); + + // read the result, in a byte array containing 4 ints + // (id, mode, size, time) + byte[] statResult = new byte[16]; + AdbHelper.read(mChannel, statResult, -1 /* full length */, AdbHelper.STD_TIMEOUT); + + // check we have the proper data back + if (checkResult(statResult, ID_STAT) == false) { + return null; + } + + // we return the mode (2nd int in the array) + return ArrayHelper.swap32bitFromArray(statResult, 4); + } catch (IOException e) { + return null; + } + } + + /** + * Create a command with a code and an int values + * @param command + * @param value + * @return + */ + private static byte[] createReq(byte[] command, int value) { + byte[] array = new byte[8]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(value, array, 4); + + return array; + } + + /** + * Creates the data array for a stat request. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...) + * @param path The path of the remote file on which to execute the command + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, String path) { + byte[] pathContent = null; + try { + pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + return createFileReq(command, pathContent); + } + + /** + * Creates the data array for a file request. This creates an array with a 4 byte command + the + * remote file name. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...). + * @param path The path, as a byte array, of the remote file on which to + * execute the command. + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, byte[] path) { + byte[] array = new byte[8 + path.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + + return array; + } + + private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) { + // make the mode into a string + String modeStr = "," + (mode & 0777); // $NON-NLS-1S + byte[] modeContent = null; + try { + modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + byte[] array = new byte[8 + path.length + modeContent.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length); + + return array; + + + } + + /** + * Checks the result array starts with the provided code + * @param result The result array to check + * @param code The 4 byte code. + * @return true if the code matches. + */ + private static boolean checkResult(byte[] result, byte[] code) { + if (result[0] != code[0] || + result[1] != code[1] || + result[2] != code[2] || + result[3] != code[3]) { + return false; + } + + return true; + + } + + private static int getFileType(int mode) { + if ((mode & S_ISOCK) == S_ISOCK) { + return FileListingService.TYPE_SOCKET; + } + + if ((mode & S_IFLNK) == S_IFLNK) { + return FileListingService.TYPE_LINK; + } + + if ((mode & S_IFREG) == S_IFREG) { + return FileListingService.TYPE_FILE; + } + + if ((mode & S_IFBLK) == S_IFBLK) { + return FileListingService.TYPE_BLOCK; + } + + if ((mode & S_IFDIR) == S_IFDIR) { + return FileListingService.TYPE_DIRECTORY; + } + + if ((mode & S_IFCHR) == S_IFCHR) { + return FileListingService.TYPE_CHARACTER; + } + + if ((mode & S_IFIFO) == S_IFIFO) { + return FileListingService.TYPE_FIFO; + } + + return FileListingService.TYPE_OTHER; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java new file mode 100644 index 0000000..8f284f3 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib; + +/** + * Holds a thread information. + */ +public final class ThreadInfo implements IStackTraceInfo { + private int mThreadId; + private String mThreadName; + private int mStatus; + private int mTid; + private int mUtime; + private int mStime; + private boolean mIsDaemon; + private StackTraceElement[] mTrace; + private long mTraceTime; + + // priority? + // total CPU used? + // method at top of stack? + + /** + * Construct with basic identification. + */ + ThreadInfo(int threadId, String threadName) { + mThreadId = threadId; + mThreadName = threadName; + + mStatus = -1; + //mTid = mUtime = mStime = 0; + //mIsDaemon = false; + } + + /** + * Set with the values we get from a THST chunk. + */ + void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) { + + mStatus = status; + mTid = tid; + mUtime = utime; + mStime = stime; + mIsDaemon = isDaemon; + } + + /** + * Sets the stack call of the thread. + * @param trace stackcall information. + */ + void setStackCall(StackTraceElement[] trace) { + mTrace = trace; + mTraceTime = System.currentTimeMillis(); + } + + /** + * Returns the thread's ID. + */ + public int getThreadId() { + return mThreadId; + } + + /** + * Returns the thread's name. + */ + public String getThreadName() { + return mThreadName; + } + + void setThreadName(String name) { + mThreadName = name; + } + + /** + * Returns the system tid. + */ + public int getTid() { + return mTid; + } + + /** + * Returns the VM thread status. + */ + public int getStatus() { + return mStatus; + } + + /** + * Returns the cumulative user time. + */ + public int getUtime() { + return mUtime; + } + + /** + * Returns the cumulative system time. + */ + public int getStime() { + return mStime; + } + + /** + * Returns whether this is a daemon thread. + */ + public boolean isDaemon() { + return mIsDaemon; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + public StackTraceElement[] getStackTrace() { + return mTrace; + } + + /** + * Returns the approximate time of the stacktrace data. + * @see #getStackTrace() + */ + public long getStackCallTime() { + return mTraceTime; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java new file mode 100644 index 0000000..ec9186c --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.LogReceiver.LogEntry; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents an event and its data. + */ +public class EventContainer { + + /** + * Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)} + * + */ + public enum CompareMethod { + EQUAL_TO("equals", "=="), + LESSER_THAN("less than or equals to", "<="), + LESSER_THAN_STRICT("less than", "<"), + GREATER_THAN("greater than or equals to", ">="), + GREATER_THAN_STRICT("greater than", ">"), + BIT_CHECK("bit check", "&"); + + private final String mName; + private final String mTestString; + + private CompareMethod(String name, String testString) { + mName = name; + mTestString = testString; + } + + /** + * Returns the display string. + */ + @Override + public String toString() { + return mName; + } + + /** + * Returns a short string representing the comparison. + */ + public String testString() { + return mTestString; + } + } + + + /** + * Type for event data. + */ + public static enum EventValueType { + UNKNOWN(0), + INT(1), + LONG(2), + STRING(3), + LIST(4), + TREE(5); + + private final static Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$ + + private int mValue; + + /** + * Returns a {@link EventValueType} from an integer value, or <code>null</code> if no match + * was found. + * @param value the integer value. + */ + static EventValueType getEventValueType(int value) { + for (EventValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + + return null; + } + + /** + * Returns a storage string for an {@link Object} of type supported by + * {@link EventValueType}. + * <p/> + * Strings created by this method can be reloaded with + * {@link #getObjectFromStorageString(String)}. + * <p/> + * NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported. + * @param object the object to "convert" into a storage string. + * @return a string storing the object and its type or null if the type was not recognized. + */ + public static String getStorageString(Object object) { + if (object instanceof String) { + return STRING.mValue + "@" + (String)object; //$NON-NLS-1$ + } else if (object instanceof Integer) { + return INT.mValue + "@" + object.toString(); //$NON-NLS-1$ + } else if (object instanceof Long) { + return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$ + } + + return null; + } + + /** + * Creates an {@link Object} from a storage string created with + * {@link #getStorageString(Object)}. + * @param value the storage string + * @return an {@link Object} or null if the string or type were not recognized. + */ + public static Object getObjectFromStorageString(String value) { + Matcher m = STORAGE_PATTERN.matcher(value); + if (m.matches()) { + try { + EventValueType type = getEventValueType(Integer.parseInt(m.group(1))); + + if (type == null) { + return null; + } + + switch (type) { + case STRING: + return m.group(2); + case INT: + return Integer.valueOf(m.group(2)); + case LONG: + return Long.valueOf(m.group(2)); + } + } catch (NumberFormatException nfe) { + return null; + } + } + + return null; + } + + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + private EventValueType(int value) { + mValue = value; + } + } + + public int mTag; + public int pid; /* generating process's pid */ + public int tid; /* generating process's tid */ + public int sec; /* seconds since Epoch */ + public int nsec; /* nanoseconds */ + + private Object mData; + + /** + * Creates an {@link EventContainer} from a {@link LogEntry}. + * @param entry the LogEntry from which pid, tid, and time info is copied. + * @param tag the event tag value + * @param data the data of the EventContainer. + */ + EventContainer(LogEntry entry, int tag, Object data) { + getType(data); + mTag = tag; + mData = data; + + pid = entry.pid; + tid = entry.tid; + sec = entry.sec; + nsec = entry.nsec; + } + + /** + * Creates an {@link EventContainer} with raw data + */ + EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + getType(data); + mTag = tag; + mData = data; + + this.pid = pid; + this.tid = tid; + this.sec = sec; + this.nsec = nsec; + } + + /** + * Returns the data as an int. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}. + * @see #getType() + */ + public final Integer getInt() throws InvalidTypeException { + if (getType(mData) == EventValueType.INT) { + return (Integer)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a long. + * @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}. + * @see #getType() + */ + public final Long getLong() throws InvalidTypeException { + if (getType(mData) == EventValueType.LONG) { + return (Long)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a String. + * @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}. + * @see #getType() + */ + public final String getString() throws InvalidTypeException { + if (getType(mData) == EventValueType.STRING) { + return (String)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns a value by index. The return type is defined by its type. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + */ + public Object getValue(int valueIndex) { + return getValue(mData, valueIndex, true); + } + + /** + * Returns a value by index as a double. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the + * list at index <code>valueIndex</code> is not of type {@link EventValueType#INT} or + * {@link EventValueType#LONG}. + * @see #getType() + */ + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return getValueAsDouble(mData, valueIndex, true); + } + + /** + * Returns a value by index as a String. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST}, + * or if the item in the list at index <code>valueIndex</code> is not of type + * {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING} + * @see #getType() + */ + public String getValueAsString(int valueIndex) throws InvalidTypeException { + return getValueAsString(mData, valueIndex, true); + } + + /** + * Returns the type of the data. + */ + public EventValueType getType() { + return getType(mData); + } + + /** + * Returns the type of an object. + */ + public final EventValueType getType(Object data) { + if (data instanceof Integer) { + return EventValueType.INT; + } else if (data instanceof Long) { + return EventValueType.LONG; + } else if (data instanceof String) { + return EventValueType.STRING; + } else if (data instanceof Object[]) { + // loop through the list to see if we have another list + Object[] objects = (Object[])data; + for (Object obj : objects) { + EventValueType type = getType(obj); + if (type == EventValueType.LIST || type == EventValueType.TREE) { + return EventValueType.TREE; + } + } + return EventValueType.LIST; + } + + return EventValueType.UNKNOWN; + } + + /** + * Checks that the <code>index</code>-th value of this event against a provided value. + * @param index the index of the value to test + * @param value the value to test against + * @param compareMethod the method of testing + * @return true if the test passed. + * @throws InvalidTypeException in case of type mismatch between the value to test and the value + * to test against, or if the compare method is incompatible with the type of the values. + * @see CompareMethod + */ + public boolean testValue(int index, Object value, + CompareMethod compareMethod) throws InvalidTypeException { + EventValueType type = getType(mData); + if (index > 0 && type != EventValueType.LIST) { + throw new InvalidTypeException(); + } + + Object data = mData; + if (type == EventValueType.LIST) { + data = ((Object[])mData)[index]; + } + + if (data.getClass().equals(data.getClass()) == false) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + return data.equals(value); + case LESSER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) <= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) <= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case LESSER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) < 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) < 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) >= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) >= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) > 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) > 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case BIT_CHECK: + if (data instanceof Integer) { + return (((Integer)data).intValue() & ((Integer)value).intValue()) != 0; + } else if (data instanceof Long) { + return (((Long)data).longValue() & ((Long)value).longValue()) != 0; + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + default : + throw new InvalidTypeException(); + } + } + + private final Object getValue(Object data, int valueIndex, boolean recursive) { + EventValueType type = getType(data); + + switch (type) { + case INT: + case LONG: + case STRING: + return data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValue(list[valueIndex], valueIndex, false); + } + } + } + + return null; + } + + private final double getValueAsDouble(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return ((Integer)data).doubleValue(); + case LONG: + return ((Long)data).doubleValue(); + case STRING: + throw new InvalidTypeException(); + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsDouble(list[valueIndex], valueIndex, false); + } + } + } + + throw new InvalidTypeException(); + } + + private final String getValueAsString(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return ((Integer)data).toString(); + case LONG: + return ((Long)data).toString(); + case STRING: + return (String)data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsString(list[valueIndex], valueIndex, false); + } + } else { + throw new InvalidTypeException( + "getValueAsString() doesn't support EventValueType.TREE"); + } + } + + throw new InvalidTypeException( + "getValueAsString() unsupported type:" + type); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java new file mode 100644 index 0000000..85e99c1 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; +import com.android.ddmlib.utils.ArrayHelper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for the "event" log. + */ +public final class EventLogParser { + + /** Location of the tag map file on the device */ + private final static String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$ + + /** + * Event log entry types. These must match up with the declarations in + * java/android/android/util/EventLog.java. + */ + private final static int EVENT_TYPE_INT = 0; + private final static int EVENT_TYPE_LONG = 1; + private final static int EVENT_TYPE_STRING = 2; + private final static int EVENT_TYPE_LIST = 3; + + private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$ + private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$ + private final static Pattern PATTERN_DESCRIPTION = Pattern.compile( + "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$ + + private final static Pattern TEXT_LOG_LINE = Pattern.compile( + "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$ + + private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>(); + + private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap = + new TreeMap<Integer, EventValueDescription[]>(); + + public EventLogParser() { + } + + /** + * Inits the parser for a specific Device. + * <p/> + * This methods reads the event-log-tags located on the device to find out + * what tags are being written to the event log and what their format is. + * @param device The device. + * @return <code>true</code> if success, <code>false</code> if failure or cancellation. + */ + public boolean init(Device device) { + // read the event tag map file on the device. + try { + device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$ + new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + processTagLine(line); + } + } + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + return false; + } + + return true; + } + + /** + * Inits the parser with the content of a tag file. + * @param tagFileContent the lines of a tag file. + * @return <code>true</code> if success, <code>false</code> if failure. + */ + public boolean init(String[] tagFileContent) { + for (String line : tagFileContent) { + processTagLine(line); + } + return true; + } + + /** + * Inits the parser with a specified event-log-tags file. + * @param filePath + * @return <code>true</code> if success, <code>false</code> if failure. + */ + public boolean init(String filePath) { + try { + BufferedReader reader = new BufferedReader(new FileReader(filePath)); + + String line = null; + do { + line = reader.readLine(); + if (line != null) { + processTagLine(line); + } + } while (line != null); + + return true; + } catch (IOException e) { + return false; + } + } + + /** + * Processes a line from the event-log-tags file. + * @param line the line to process + */ + private void processTagLine(String line) { + // ignore empty lines and comment lines + if (line.length() > 0 && line.charAt(0) != '#') { + Matcher m = PATTERN_TAG_WITH_DESC.matcher(line); + if (m.matches()) { + try { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + + // special case for the GC tag. We ignore what is in the file, + // and take what the custom GcEventContainer class tells us. + // This is due to the event encoding several values on 2 longs. + // @see GcEventContainer + if (value == GcEventContainer.GC_EVENT_TAG) { + mValueDescriptionMap.put(value, + GcEventContainer.getValueDescriptions()); + } else { + + String description = m.group(3); + if (description != null && description.length() > 0) { + EventValueDescription[] desc = + processDescription(description); + + if (desc != null) { + mValueDescriptionMap.put(value, desc); + } + } + } + } catch (NumberFormatException e) { + // failed to convert the number into a string. just ignore it. + } + } else { + m = PATTERN_SIMPLE_TAG.matcher(line); + if (m.matches()) { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + } + } + } + } + + private EventValueDescription[] processDescription(String description) { + String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>(); + + for (String desc : descriptions) { + Matcher m = PATTERN_DESCRIPTION.matcher(desc); + if (m.matches()) { + try { + String name = m.group(1); + + String typeString = m.group(2); + int typeValue = Integer.parseInt(typeString); + EventValueType eventValueType = EventValueType.getEventValueType(typeValue); + if (eventValueType == null) { + // just ignore this description if the value is not recognized. + // TODO: log the error. + } + + typeString = m.group(3); + if (typeString != null && typeString.length() > 0) { + //skip the | + typeString = typeString.substring(1); + + typeValue = Integer.parseInt(typeString); + ValueType valueType = ValueType.getValueType(typeValue); + + list.add(new EventValueDescription(name, eventValueType, valueType)); + } else { + list.add(new EventValueDescription(name, eventValueType)); + } + } catch (NumberFormatException nfe) { + // just ignore this description if one number is malformed. + // TODO: log the error. + } catch (InvalidValueTypeException e) { + // just ignore this description if data type and data unit don't match + // TODO: log the error. + } + } else { + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Can't parse %1$s", description)); //$NON-NLS-1$ + } + } + + if (list.size() == 0) { + return null; + } + + return list.toArray(new EventValueDescription[list.size()]); + + } + + public EventContainer parse(LogEntry entry) { + if (entry.len < 4) { + return null; + } + + int inOffset = 0; + + int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset); + inOffset += 4; + + String tag = mTagMap.get(tagValue); + if (tag == null) { + Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue)); + } + + ArrayList<Object> list = new ArrayList<Object>(); + if (parseBinaryEvent(entry.data, inOffset, list) == -1) { + return null; + } + + Object data; + if (list.size() == 1) { + data = list.get(0); + } else{ + data = list.toArray(); + } + + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(entry, tagValue, data); + } else { + event = new EventContainer(entry, tagValue, data); + } + + return event; + } + + public EventContainer parse(String textLogLine) { + // line will look like + // 04-29 23:16:16.691 I/dvm_gc_info( 427): <data> + // where <data> is either + // [value1,value2...] + // or + // value + if (textLogLine.length() == 0) { + return null; + } + + // parse the header first + Matcher m = TEXT_LOG_LINE.matcher(textLogLine); + if (m.matches()) { + try { + int month = Integer.parseInt(m.group(1)); + int day = Integer.parseInt(m.group(2)); + int hours = Integer.parseInt(m.group(3)); + int minutes = Integer.parseInt(m.group(4)); + int seconds = Integer.parseInt(m.group(5)); + int milliseconds = Integer.parseInt(m.group(6)); + + // convert into seconds since epoch and nano-seconds. + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds); + int sec = (int)Math.floor(cal.getTimeInMillis()/1000); + int nsec = milliseconds * 1000000; + + String tag = m.group(7); + + // get the numerical tag value + int tagValue = -1; + Set<Entry<Integer, String>> tagSet = mTagMap.entrySet(); + for (Entry<Integer, String> entry : tagSet) { + if (tag.equals(entry.getValue())) { + tagValue = entry.getKey(); + break; + } + } + + if (tagValue == -1) { + return null; + } + + int pid = Integer.parseInt(m.group(8)); + + Object data = parseTextData(m.group(9), tagValue); + if (data == null) { + return null; + } + + // now we can allocate and return the EventContainer + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } else { + event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } + + return event; + } catch (NumberFormatException e) { + return null; + } + } + + return null; + } + + public Map<Integer, String> getTagMap() { + return mTagMap; + } + + public Map<Integer, EventValueDescription[]> getEventInfoMap() { + return mValueDescriptionMap; + } + + /** + * Recursively convert binary log data to printable form. + * + * This needs to be recursive because you can have lists of lists. + * + * If we run out of room, we stop processing immediately. It's important + * for us to check for space on every output element to avoid producing + * garbled output. + * + * Returns the amount read on success, -1 on failure. + */ + private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) { + + if (eventData.length - dataOffset < 1) + return -1; + + int offset = dataOffset; + + int type = eventData[offset++]; + + //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen); + + switch (type) { + case EVENT_TYPE_INT: { /* 32-bit signed int */ + int ival; + + if (eventData.length - offset < 4) + return -1; + ival = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + list.add(new Integer(ival)); + } + break; + case EVENT_TYPE_LONG: { /* 64-bit signed long */ + long lval; + + if (eventData.length - offset < 8) + return -1; + lval = ArrayHelper.swap64bitFromArray(eventData, offset); + offset += 8; + + list.add(new Long(lval)); + } + break; + case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */ + int strLen; + + if (eventData.length - offset < 4) + return -1; + strLen = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + if (eventData.length - offset < strLen) + return -1; + + // get the string + try { + String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$ + list.add(str); + } catch (UnsupportedEncodingException e) { + } + offset += strLen; + break; + } + case EVENT_TYPE_LIST: { /* N items, all different types */ + + if (eventData.length - offset < 1) + return -1; + + int count = eventData[offset++]; + + // make a new temp list + ArrayList<Object> subList = new ArrayList<Object>(); + for (int i = 0; i < count; i++) { + int result = parseBinaryEvent(eventData, offset, subList); + if (result == -1) { + return result; + } + + offset += result; + } + + list.add(subList.toArray()); + } + break; + default: + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Unknown binary event type %1$d", type)); //$NON-NLS-1$ + return -1; + } + + return offset - dataOffset; + } + + private Object parseTextData(String data, int tagValue) { + // first, get the description of what we're supposed to parse + EventValueDescription[] desc = mValueDescriptionMap.get(tagValue); + + if (desc == null) { + // TODO parse and create string values. + return null; + } + + if (desc.length == 1) { + return getObjectFromString(data, desc[0].getEventValueType()); + } else if (data.startsWith("[") && data.endsWith("]")) { + data = data.substring(1, data.length() - 1); + + // get each individual values as String + String[] values = data.split(","); + + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + // special case for the GC event! + Object[] objects = new Object[2]; + + objects[0] = getObjectFromString(values[0], EventValueType.LONG); + objects[1] = getObjectFromString(values[1], EventValueType.LONG); + + return objects; + } else { + // must be the same number as the number of descriptors. + if (values.length != desc.length) { + return null; + } + + Object[] objects = new Object[values.length]; + + for (int i = 0 ; i < desc.length ; i++) { + Object obj = getObjectFromString(values[i], desc[i].getEventValueType()); + if (obj == null) { + return null; + } + objects[i] = obj; + } + + return objects; + } + } + + return null; + } + + + private Object getObjectFromString(String value, EventValueType type) { + try { + switch (type) { + case INT: + return Integer.valueOf(value); + case LONG: + return Long.valueOf(value); + case STRING: + return value; + } + } catch (NumberFormatException e) { + // do nothing, we'll return null. + } + + return null; + } + + /** + * Recreates the event-log-tags at the specified file path. + * @param filePath the file path to write the file. + * @throws IOException + */ + public void saveTags(String filePath) throws IOException { + File destFile = new File(filePath); + destFile.createNewFile(); + FileOutputStream fos = null; + + try { + + fos = new FileOutputStream(destFile); + + for (Integer key : mTagMap.keySet()) { + // get the tag name + String tagName = mTagMap.get(key); + + // get the value descriptions + EventValueDescription[] descriptors = mValueDescriptionMap.get(key); + + String line = null; + if (descriptors != null) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$ + boolean first = true; + for (EventValueDescription evd : descriptors) { + if (first) { + sb.append(" ("); //$NON-NLS-1$ + first = false; + } else { + sb.append(",("); //$NON-NLS-1$ + } + sb.append(evd.getName()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getEventValueType().getValue()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getValueType().getValue()); + sb.append("|)"); //$NON-NLS-1$ + } + sb.append("\n"); //$NON-NLS-1$ + + line = sb.toString(); + } else { + line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$ + } + + byte[] buffer = line.getBytes(); + fos.write(buffer); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } + + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java new file mode 100644 index 0000000..b68b4e8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; + + +/** + * Describes an {@link EventContainer} value. + * <p/> + * This is a stand-alone object, not linked to a particular Event. It describes the value, by + * name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}). + * <p/> + * The index of the value is not contained within this class, and is instead dependent on the + * index of this particular object in the array of {@link EventValueDescription} returned by + * {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag. + * + */ +public final class EventValueDescription { + + /** + * Represents the type of a numerical value. This is used to display values of vastly different + * type/range in graphs. + */ + public static enum ValueType { + NOT_APPLICABLE(0), + OBJECTS(1), + BYTES(2), + MILLISECONDS(3), + ALLOCATIONS(4), + ID(5), + PERCENT(6); + + private int mValue; + + /** + * Checks that the {@link EventValueType} is compatible with the {@link ValueType}. + * @param type the {@link EventValueType} to check. + * @throws InvalidValueTypeException if the types are not compatible. + */ + public void checkType(EventValueType type) throws InvalidValueTypeException { + if ((type != EventValueType.INT && type != EventValueType.LONG) + && this != NOT_APPLICABLE) { + throw new InvalidValueTypeException( + String.format("%1$s doesn't support type %2$s", type, this)); + } + } + + /** + * Returns a {@link ValueType} from an integer value, or <code>null</code> if no match + * were found. + * @param value the integer value. + */ + public static ValueType getValueType(int value) { + for (ValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + return null; + } + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + private ValueType(int value) { + mValue = value; + } + } + + private String mName; + private EventValueType mEventValueType; + private ValueType mValueType; + + /** + * Builds a {@link EventValueDescription} with a name and a type. + * <p/> + * If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the + * {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to + * {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values. + * @param name + * @param type + */ + EventValueDescription(String name, EventValueType type) { + mName = name; + mEventValueType = type; + if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) { + mValueType = ValueType.BYTES; + } else { + mValueType = ValueType.NOT_APPLICABLE; + } + } + + /** + * Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}. + * <p/> + * @param name + * @param type + * @param valueType + * @throws InvalidValueTypeException if type and valuetype are not compatible. + * + */ + EventValueDescription(String name, EventValueType type, ValueType valueType) + throws InvalidValueTypeException { + mName = name; + mEventValueType = type; + mValueType = valueType; + mValueType.checkType(mEventValueType); + } + + /** + * @return the Name. + */ + public String getName() { + return mName; + } + + /** + * @return the {@link EventValueType}. + */ + public EventValueType getEventValueType() { + return mEventValueType; + } + + /** + * @return the {@link ValueType}. + */ + public ValueType getValueType() { + return mValueType; + } + + @Override + public String toString() { + if (mValueType != ValueType.NOT_APPLICABLE) { + return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(), + mValueType.toString()); + } + + return String.format("%1$s (%2$s)", mName, mEventValueType.toString()); + } + + /** + * Checks if the value is of the proper type for this receiver. + * @param value the value to check. + * @return true if the value is of the proper type for this receiver. + */ + public boolean checkForType(Object value) { + switch (mEventValueType) { + case INT: + return value instanceof Integer; + case LONG: + return value instanceof Long; + case STRING: + return value instanceof String; + case LIST: + return value instanceof Object[]; + } + + return false; + } + + /** + * Returns an object of a valid type (based on the value returned by + * {@link #getEventValueType()}) from a String value. + * <p/> + * IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not + * supported. + * @param value the value of the object expressed as a string. + * @return an object or null if the conversion could not be done. + */ + public Object getObjectFromString(String value) { + switch (mEventValueType) { + case INT: + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case LONG: + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case STRING: + return value; + } + + return null; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java new file mode 100644 index 0000000..7bae202 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; + +/** + * Custom Event Container for the Gc event since this event doesn't simply output data in + * int or long format, but encodes several values on 4 longs. + * <p/> + * The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must + * be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used. + */ +final class GcEventContainer extends EventContainer { + + public final static int GC_EVENT_TAG = 20001; + + private String processId; + private long gcTime; + private long bytesFreed; + private long objectsFreed; + private long actualSize; + private long allowedSize; + private long softLimit; + private long objectsAllocated; + private long bytesAllocated; + private long zActualSize; + private long zAllowedSize; + private long zObjectsAllocated; + private long zBytesAllocated; + private long dlmallocFootprint; + private long mallinfoTotalAllocatedSpace; + private long externalLimit; + private long externalBytesAllocated; + + GcEventContainer(LogEntry entry, int tag, Object data) { + super(entry, tag, data); + init(data); + } + + GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + super(tag, pid, tid, sec, nsec, data); + init(data); + } + + /** + * @param data + */ + private void init(Object data) { + if (data instanceof Object[]) { + Object[] values = (Object[])data; + for (int i = 0; i < values.length; i++) { + if (values[i] instanceof Long) { + parseDvmHeapInfo((Long)values[i], i); + } + } + } + } + + @Override + public EventValueType getType() { + return EventValueType.LIST; + } + + @Override + public boolean testValue(int index, Object value, CompareMethod compareMethod) + throws InvalidTypeException { + // do a quick easy check on the type. + if (index == 0) { + if ((value instanceof String) == false) { + throw new InvalidTypeException(); + } + } else if ((value instanceof Long) == false) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + if (index == 0) { + return processId.equals(value); + } else { + return getValueAsLong(index) == ((Long)value).longValue(); + } + case LESSER_THAN: + return getValueAsLong(index) <= ((Long)value).longValue(); + case LESSER_THAN_STRICT: + return getValueAsLong(index) < ((Long)value).longValue(); + case GREATER_THAN: + return getValueAsLong(index) >= ((Long)value).longValue(); + case GREATER_THAN_STRICT: + return getValueAsLong(index) > ((Long)value).longValue(); + case BIT_CHECK: + return (getValueAsLong(index) & ((Long)value).longValue()) != 0; + } + + throw new ArrayIndexOutOfBoundsException(); + } + + @Override + public Object getValue(int valueIndex) { + if (valueIndex == 0) { + return processId; + } + + try { + return new Long(getValueAsLong(valueIndex)); + } catch (InvalidTypeException e) { + // this would only happened if valueIndex was 0, which we test above. + } + + return null; + } + + @Override + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return (double)getValueAsLong(valueIndex); + } + + @Override + public String getValueAsString(int valueIndex) { + switch (valueIndex) { + case 0: + return processId; + default: + try { + return Long.toString(getValueAsLong(valueIndex)); + } catch (InvalidTypeException e) { + // we shouldn't stop there since we test, in this method first. + } + } + + throw new ArrayIndexOutOfBoundsException(); + } + + /** + * Returns a custom array of {@link EventValueDescription} since the actual content of this + * event (list of (long, long) does not match the values encoded into those longs. + */ + static EventValueDescription[] getValueDescriptions() { + try { + return new EventValueDescription[] { + new EventValueDescription("Process Name", EventValueType.STRING), + new EventValueDescription("GC Time", EventValueType.LONG, + ValueType.MILLISECONDS), + new EventValueDescription("Freed Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (aggregate)", + EventValueType.LONG, ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allocated Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Allocation Limit", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Bytes Allocated", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("dlmalloc Footprint", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Malloc Info: Total Allocated Space", + EventValueType.LONG, ValueType.BYTES), + }; + } catch (InvalidValueTypeException e) { + // this shouldn't happen since we control manual the EventValueType and the ValueType + // values. For development purpose, we assert if this happens. + assert false; + } + + // this shouldn't happen, but the compiler complains otherwise. + return null; + } + + private void parseDvmHeapInfo(long data, int index) { + switch (index) { + case 0: + // [63 ] Must be zero + // [62-24] ASCII process identifier + // [23-12] GC time in ms + // [11- 0] Bytes freed + + gcTime = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesFreed = float12ToInt((int)(data & 0xFFFL)); + + // convert the long into an array, in the proper order so that we can convert the + // first 5 char into a string. + byte[] dataArray = new byte[8]; + put64bitsToArray(data, dataArray, 0); + + // get the name from the string + processId = new String(dataArray, 0, 5); + break; + case 1: + // [63-62] 10 + // [61-60] Reserved; must be zero + // [59-48] Objects freed + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL)); + actualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 2: + // [63-62] 11 + // [61-60] Reserved; must be zero + // [59-48] Soft limit (current soft max) + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + softLimit = float12ToInt((int)((data >> 48) & 0xFFFL)); + zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + zBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 3: + // [63-48] Reserved; must be zero + // [47-36] dlmallocFootprint + // [35-24] mallinfo: total allocated space + // [23-12] External byte limit + // [11- 0] External bytes allocated + dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL)); + mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL)); + externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL)); + externalBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + default: + break; + } + } + + /** + * Converts a 12 bit float representation into an unsigned int (returned as a long) + * @param f12 + */ + private static long float12ToInt(int f12) { + return (f12 & 0x1FF) << ((f12 >>> 9) * 4); + } + + /** + * puts an unsigned value in an array. + * @param value The value to put. + * @param dest the destination array + * @param offset the offset in the array where to put the value. + * Array length must be at least offset + 8 + */ + private static void put64bitsToArray(long value, byte[] dest, int offset) { + dest[offset + 7] = (byte)(value & 0x00000000000000FFL); + dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8); + dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16); + dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24); + dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32); + dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40); + dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48); + dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56); + } + + /** + * Returns the long value of the <code>valueIndex</code>-th value. + * @param valueIndex the index of the value. + * @throws InvalidTypeException if index is 0 as it is a string value. + */ + private final long getValueAsLong(int valueIndex) throws InvalidTypeException { + switch (valueIndex) { + case 0: + throw new InvalidTypeException(); + case 1: + return gcTime; + case 2: + return objectsFreed; + case 3: + return bytesFreed; + case 4: + return softLimit; + case 5: + return actualSize; + case 6: + return allowedSize; + case 7: + return objectsAllocated; + case 8: + return bytesAllocated; + case 9: + return actualSize - zActualSize; + case 10: + return allowedSize - zAllowedSize; + case 11: + return objectsAllocated - zObjectsAllocated; + case 12: + return bytesAllocated - zBytesAllocated; + case 13: + return zActualSize; + case 14: + return zAllowedSize; + case 15: + return zObjectsAllocated; + case 16: + return zBytesAllocated; + case 17: + return externalLimit; + case 18: + return externalBytesAllocated; + case 19: + return dlmallocFootprint; + case 20: + return mallinfoTotalAllocatedSpace; + } + + throw new ArrayIndexOutOfBoundsException(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java new file mode 100644 index 0000000..016f8aa --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import java.io.Serializable; + +/** + * Exception thrown when accessing an {@link EventContainer} value with the wrong type. + */ +public final class InvalidTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * <code>(cause==null ? null : cause.toString())</code> (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java new file mode 100644 index 0000000..a3050c8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; + +import java.io.Serializable; + +/** + * Exception thrown when associating an {@link EventValueType} with an incompatible + * {@link ValueType}. + */ +public final class InvalidValueTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidValueTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * <code>(cause==null ? null : cause.toString())</code> (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java new file mode 100644 index 0000000..b49f025 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.log; + + +import com.android.ddmlib.utils.ArrayHelper; + +import java.security.InvalidParameterException; + +/** + * Receiver able to provide low level parsing for device-side log services. + */ +public final class LogReceiver { + + private final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry. + + /** + * Represents a log entry and its raw data. + */ + public final static class LogEntry { + /* + * See //device/include/utils/logger.h + */ + /** 16bit unsigned: length of the payload. */ + public int len; /* This is normally followed by a 16 bit padding */ + /** pid of the process that generated this {@link LogEntry} */ + public int pid; + /** tid of the process that generated this {@link LogEntry} */ + public int tid; + /** Seconds since epoch. */ + public int sec; + /** nanoseconds. */ + public int nsec; + /** The entry's raw data. */ + public byte[] data; + }; + + /** + * Classes which implement this interface provide a method that deals + * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}. + * <p/>This interface provides two methods. + * <ul> + * <li>{@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a + * first level of parsing, extracting {@link LogEntry} objects out of the log service output.</li> + * <li>{@link #newData(byte[], int, int)} provides a way to receive the raw information + * coming directly from the log service.</li> + * </ul> + */ + public interface ILogListener { + /** + * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}. + * @param entry the new log entry. + */ + public void newEntry(LogEntry entry); + + /** + * Sent when new raw data is coming from the log service. + * @param data the raw data buffer. + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void newData(byte[] data, int offset, int length); + } + + /** Current {@link LogEntry} being read, before sending it to the listener. */ + private LogEntry mCurrentEntry; + + /** Temp buffer to store partial entry headers. */ + private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE]; + /** Offset in the partial header buffer */ + private int mEntryHeaderOffset = 0; + /** Offset in the partial entry data */ + private int mEntryDataOffset = 0; + + /** Listener waiting for receive fully read {@link LogEntry} objects */ + private ILogListener mListener; + + private boolean mIsCancelled = false; + + /** + * Creates a {@link LogReceiver} with an {@link ILogListener}. + * <p/> + * The {@link ILogListener} will receive new log entries as they are parsed, in the form + * of {@link LogEntry} objects. + * @param listener the listener to receive new log entries. + */ + public LogReceiver(ILogListener listener) { + mListener = listener; + } + + + /** + * Parses new data coming from the log service. + * @param data the data buffer + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void parseNewData(byte[] data, int offset, int length) { + // notify the listener of new raw data + if (mListener != null) { + mListener.newData(data, offset, length); + } + + // loop while there is still data to be read and the receiver has not be cancelled. + while (length > 0 && mIsCancelled == false) { + // first check if we have no current entry. + if (mCurrentEntry == null) { + if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) { + // if we don't have enough data to finish the header, save + // the data we have and return + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length); + mEntryHeaderOffset += length; + return; + } else { + // we have enough to fill the header, let's do it. + // did we store some part at the beginning of the header? + if (mEntryHeaderOffset != 0) { + // copy the rest of the entry header into the header buffer + int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset; + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, + size); + + // create the entry from the header buffer + mCurrentEntry = createEntry(mEntryHeaderBuffer, 0); + + // since we used the whole entry header buffer, we reset the offset + mEntryHeaderOffset = 0; + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += size; + length -= size; + } else { + // create the entry directly from the data array + mCurrentEntry = createEntry(data, offset); + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += ENTRY_HEADER_SIZE; + length -= ENTRY_HEADER_SIZE; + } + } + } + + // at this point, we have an entry, and offset/length have been updated to skip + // the entry header. + + // if we have enough data for this entry or more, we'll need to end this entry + if (length >= mCurrentEntry.len - mEntryDataOffset) { + // compute and save the size of the data that we have to read for this entry, + // based on how much we may already have read. + int dataSize = mCurrentEntry.len - mEntryDataOffset; + + // we only read what we need, and put it in the entry buffer. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize); + + // notify the listener of a new entry + if (mListener != null) { + mListener.newEntry(mCurrentEntry); + } + + // reset some flags: we have read 0 data of the current entry. + // and we have no current entry being read. + mEntryDataOffset = 0; + mCurrentEntry = null; + + // and update the data buffer info to the end of the current entry / start + // of the next one. + offset += dataSize; + length -= dataSize; + } else { + // we don't have enough data to fill this entry, so we store what we have + // in the entry itself. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length); + + // save the amount read for the data. + mEntryDataOffset += length; + return; + } + } + } + + /** + * Returns whether this receiver is canceling the remote service. + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Cancels the current remote service. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size + * to be at least <code>offset + {@link #ENTRY_HEADER_SIZE}</code>. + * @param data the data buffer the entry is read from. + * @param offset the offset of the first byte from the buffer representing the entry. + * @return a new {@link LogEntry} or <code>null</code> if some error happened. + */ + private LogEntry createEntry(byte[] data, int offset) { + if (data.length < offset + ENTRY_HEADER_SIZE) { + throw new InvalidParameterException( + "Buffer not big enough to hold full LoggerEntry header"); + } + + // create the new entry and fill it. + LogEntry entry = new LogEntry(); + entry.len = ArrayHelper.swapU16bitFromArray(data, offset); + + // we've read only 16 bits, but since there's also a 16 bit padding, + // we can skip right over both. + offset += 4; + + entry.pid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.tid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.sec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.nsec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + + // allocate the data + entry.data = new byte[entry.len]; + + return entry; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java new file mode 100644 index 0000000..b61a698 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +/** + * Receives event notifications during instrumentation test runs. + * Patterned after {@link junit.runner.TestRunListener}. + */ +public interface ITestRunListener { + + /** + * Types of test failures. + */ + enum TestFailure { + /** Test failed due to unanticipated uncaught exception. */ + ERROR, + /** Test failed due to a false assertion. */ + FAILURE + } + + /** + * Reports the start of a test run. + * + * @param testCount total number of tests in test run + */ + public void testRunStarted(int testCount); + + /** + * Reports end of test run. + * + * @param elapsedTime device reported elapsed time, in milliseconds + */ + public void testRunEnded(long elapsedTime); + + /** + * Reports test run stopped before completion. + * + * @param elapsedTime device reported elapsed time, in milliseconds + */ + public void testRunStopped(long elapsedTime); + + /** + * Reports the start of an individual test case. + * + * @param test identifies the test + */ + public void testStarted(TestIdentifier test); + + /** + * Reports the execution end of an individual test case. + * If {@link #testFailed} was not invoked, this test passed. + * + * @param test identifies the test + */ + public void testEnded(TestIdentifier test); + + /** + * Reports the failure of a individual test case. + * Will be called between testStarted and testEnded. + * + * @param status failure type + * @param test identifies the test + * @param trace stack trace of failure + */ + public void testFailed(TestFailure status, TestIdentifier test, String trace); + + /** + * Reports test run failed to execute due to a fatal error. + */ + public void testRunFailed(String errorMessage); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java new file mode 100755 index 0000000..bc1834f --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; + +/** + * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a + * ITestRunListener of the results. + * + * <p>Expects the following output: + * + * <p>If fatal error occurred when attempted to run the tests: + * <pre> INSTRUMENTATION_FAILED: </pre> + * + * <p>Otherwise, expect a series of test results, each one containing a set of status key/value + * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test + * run, expects that the elapsed test time in seconds will be displayed + * + * <p>For example: + * <pre> + * INSTRUMENTATION_STATUS_CODE: 1 + * INSTRUMENTATION_STATUS: class=com.foo.FooTest + * INSTRUMENTATION_STATUS: test=testFoo + * INSTRUMENTATION_STATUS: numtests=2 + * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312 + * com.foo.X + * INSTRUMENTATION_STATUS_CODE: -2 + * ... + * + * Time: X + * </pre> + * <p>Note that the "value" portion of the key-value pair may wrap over several text lines + */ +public class InstrumentationResultParser extends MultiLineReceiver { + + /** Relevant test status keys. */ + private static class StatusKeys { + private static final String TEST = "test"; + private static final String CLASS = "class"; + private static final String STACK = "stack"; + private static final String NUMTESTS = "numtests"; + } + + /** Test result status codes. */ + private static class StatusCodes { + private static final int FAILURE = -2; + private static final int START = 1; + private static final int ERROR = -1; + private static final int OK = 0; + } + + /** Prefixes used to identify output. */ + private static class Prefixes { + private static final String STATUS = "INSTRUMENTATION_STATUS: "; + private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: "; + private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; + private static final String TIME_REPORT = "Time: "; + } + + private final ITestRunListener mTestListener; + + /** + * Test result data + */ + private static class TestResult { + private Integer mCode = null; + private String mTestName = null; + private String mTestClass = null; + private String mStackTrace = null; + private Integer mNumTests = null; + + /** Returns true if all expected values have been parsed */ + boolean isComplete() { + return mCode != null && mTestName != null && mTestClass != null; + } + } + + /** Stores the status values for the test result currently being parsed */ + private TestResult mCurrentTestResult = null; + + /** Stores the current "key" portion of the status key-value being parsed. */ + private String mCurrentKey = null; + + /** Stores the current "value" portion of the status key-value being parsed. */ + private StringBuilder mCurrentValue = null; + + /** True if start of test has already been reported to listener. */ + private boolean mTestStartReported = false; + + /** The elapsed time of the test run, in milliseconds. */ + private long mTestTime = 0; + + /** True if current test run has been canceled by user. */ + private boolean mIsCancelled = false; + + private static final String LOG_TAG = "InstrumentationResultParser"; + + /** + * Creates the InstrumentationResultParser. + * + * @param listener informed of test results as the tests are executing + */ + public InstrumentationResultParser(ITestRunListener listener) { + mTestListener = listener; + } + + /** + * Processes the instrumentation test output from shell. + * + * @see MultiLineReceiver#processNewLines + */ + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + parse(line); + } + } + + /** + * Parse an individual output line. Expects a line that is one of: + * <ul> + * <li> + * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE), + * and thus there is a new key=value pair to parse, and the previous key-value pair is + * finished. + * </li> + * <li> + * A continuation of the previous status (the "value" portion of the key has wrapped + * to the next line). + * </li> + * <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li> + * <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li> + * </ul> + * + * @param line Text output line + */ + private void parse(String line) { + if (line.startsWith(Prefixes.STATUS_CODE)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + parseStatusCode(line); + } else if (line.startsWith(Prefixes.STATUS)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + parseKey(line, Prefixes.STATUS.length()); + } else if (line.startsWith(Prefixes.STATUS_FAILED)) { + Log.e(LOG_TAG, "test run failed " + line); + mTestListener.testRunFailed(line); + } else if (line.startsWith(Prefixes.TIME_REPORT)) { + parseTime(line, Prefixes.TIME_REPORT.length()); + } else { + if (mCurrentValue != null) { + // this is a value that has wrapped to next line. + mCurrentValue.append("\r\n"); + mCurrentValue.append(line); + } else { + Log.w(LOG_TAG, "unrecognized line " + line); + } + } + } + + /** + * Stores the currently parsed key-value pair into mCurrentTestInfo. + */ + private void submitCurrentKeyValue() { + if (mCurrentKey != null && mCurrentValue != null) { + TestResult testInfo = getCurrentTestInfo(); + String statusValue = mCurrentValue.toString(); + + if (mCurrentKey.equals(StatusKeys.CLASS)) { + testInfo.mTestClass = statusValue.trim(); + } + else if (mCurrentKey.equals(StatusKeys.TEST)) { + testInfo.mTestName = statusValue.trim(); + } + else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) { + try { + testInfo.mNumTests = Integer.parseInt(statusValue); + } + catch (NumberFormatException e) { + Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue); + } + } + else if (mCurrentKey.equals(StatusKeys.STACK)) { + testInfo.mStackTrace = statusValue; + } + + mCurrentKey = null; + mCurrentValue = null; + } + } + + private TestResult getCurrentTestInfo() { + if (mCurrentTestResult == null) { + mCurrentTestResult = new TestResult(); + } + return mCurrentTestResult; + } + + private void clearCurrentTestInfo() { + mCurrentTestResult = null; + } + + /** + * Parses the key from the current line. + * Expects format of "key=value". + * + * @param line full line of text to parse + * @param keyStartPos the starting position of the key in the given line + */ + private void parseKey(String line, int keyStartPos) { + int endKeyPos = line.indexOf('=', keyStartPos); + if (endKeyPos != -1) { + mCurrentKey = line.substring(keyStartPos, endKeyPos).trim(); + parseValue(line, endKeyPos+1); + } + } + + /** + * Parses the start of a key=value pair. + * + * @param line - full line of text to parse + * @param valueStartPos - the starting position of the value in the given line + */ + private void parseValue(String line, int valueStartPos) { + mCurrentValue = new StringBuilder(); + mCurrentValue.append(line.substring(valueStartPos)); + } + + /** + * Parses out a status code result. + */ + private void parseStatusCode(String line) { + String value = line.substring(Prefixes.STATUS_CODE.length()).trim(); + TestResult testInfo = getCurrentTestInfo(); + try { + testInfo.mCode = Integer.parseInt(value); + } + catch (NumberFormatException e) { + Log.e(LOG_TAG, "Expected integer status code, received: " + value); + } + + // this means we're done with current test result bundle + reportResult(testInfo); + clearCurrentTestInfo(); + } + + /** + * Returns true if test run canceled. + * + * @see IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Requests cancellation of test run. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Reports a test result to the test run listener. Must be called when a individual test + * result has been fully parsed. + * + * @param statusMap key-value status pairs of test result + */ + private void reportResult(TestResult testInfo) { + if (!testInfo.isComplete()) { + Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString()); + return; + } + reportTestRunStarted(testInfo); + TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName); + + switch (testInfo.mCode) { + case StatusCodes.START: + mTestListener.testStarted(testId); + break; + case StatusCodes.FAILURE: + mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.ERROR: + mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.OK: + mTestListener.testEnded(testId); + break; + default: + Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode); + mTestListener.testEnded(testId); + break; + } + + } + + /** + * Reports the start of a test run, and the total test count, if it has not been previously + * reported. + * + * @param testInfo current test status values + */ + private void reportTestRunStarted(TestResult testInfo) { + // if start test run not reported yet + if (!mTestStartReported && testInfo.mNumTests != null) { + mTestListener.testRunStarted(testInfo.mNumTests); + mTestStartReported = true; + } + } + + /** + * Returns the stack trace of the current failed test, from the provided testInfo. + */ + private String getTrace(TestResult testInfo) { + if (testInfo.mStackTrace != null) { + return testInfo.mStackTrace; + } + else { + Log.e(LOG_TAG, "Could not find stack trace for failed test "); + return new Throwable("Unknown failure").toString(); + } + } + + /** + * Parses out and store the elapsed time. + */ + private void parseTime(String line, int startPos) { + String timeString = line.substring(startPos); + try { + float timeSeconds = Float.parseFloat(timeString); + mTestTime = (long)(timeSeconds * 1000); + } + catch (NumberFormatException e) { + Log.e(LOG_TAG, "Unexpected time format " + timeString); + } + } + + /** + * Called by parent when adb session is complete. + */ + @Override + public void done() { + super.done(); + mTestListener.testRunEnded(mTestTime); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java new file mode 100644 index 0000000..4edbbbb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; + +import java.io.IOException; + +/** + * Runs a Android test command remotely and reports results. + */ +public class RemoteAndroidTestRunner { + + private static final char CLASS_SEPARATOR = ','; + private static final char METHOD_SEPARATOR = '#'; + private static final char RUNNER_SEPARATOR = '/'; + private String mClassArg; + private final String mPackageName; + private final String mRunnerName; + private String mExtraArgs; + private boolean mLogOnlyMode; + private IDevice mRemoteDevice; + private InstrumentationResultParser mParser; + + private static final String LOG_TAG = "RemoteAndroidTest"; + private static final String DEFAULT_RUNNER_NAME = + "android.test.InstrumentationTestRunner"; + + /** + * Creates a remote Android test runner. + * + * @param packageName the Android application package that contains the tests to run + * @param runnerName the instrumentation test runner to execute. If null, will use default + * runner + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + String runnerName, + IDevice remoteDevice) { + + mPackageName = packageName; + mRunnerName = runnerName; + mRemoteDevice = remoteDevice; + mClassArg = null; + mExtraArgs = ""; + mLogOnlyMode = false; + } + + /** + * Alternate constructor. Uses default instrumentation runner. + * + * @param packageName the Android application package that contains the tests to run + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + IDevice remoteDevice) { + this(packageName, null, remoteDevice); + } + + /** + * Returns the application package name. + */ + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the runnerName. + */ + public String getRunnerName() { + if (mRunnerName == null) { + return DEFAULT_RUNNER_NAME; + } + return mRunnerName; + } + + /** + * Returns the complete instrumentation component path. + */ + private String getRunnerPath() { + return getPackageName() + RUNNER_SEPARATOR + getRunnerName(); + } + + /** + * Sets to run only tests in this class + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + */ + public void setClassName(String className) { + mClassArg = className; + } + + /** + * Sets to run only tests in the provided classes + * Must be called before 'run'. + * <p> + * If providing more than one class, requires a InstrumentationTestRunner that supports + * the multiple class argument syntax. + * + * @param classNames array of fully qualified class names (eg x.y.z) + */ + public void setClassNames(String[] classNames) { + StringBuilder classArgBuilder = new StringBuilder(); + + for (int i=0; i < classNames.length; i++) { + if (i != 0) { + classArgBuilder.append(CLASS_SEPARATOR); + } + classArgBuilder.append(classNames[i]); + } + mClassArg = classArgBuilder.toString(); + } + + /** + * Sets to run only specified test method + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + * @param testName method name + */ + public void setMethodName(String className, String testName) { + mClassArg = className + METHOD_SEPARATOR + testName; + } + + /** + * Sets extra arguments to include in instrumentation command. + * Must be called before 'run'. + * + * @param instrumentationArgs must not be null + */ + public void setExtraArgs(String instrumentationArgs) { + if (instrumentationArgs == null) { + throw new IllegalArgumentException("instrumentationArgs cannot be null"); + } + mExtraArgs = instrumentationArgs; + } + + /** + * Returns the extra instrumentation arguments. + */ + public String getExtraArgs() { + return mExtraArgs; + } + + /** + * Sets this test run to log only mode - skips test execution. + */ + public void setLogOnly(boolean logOnly) { + mLogOnlyMode = logOnly; + } + + /** + * Execute this test run. + * + * @param listener listens for test results + */ + public void run(ITestRunListener listener) { + final String runCaseCommandStr = "am instrument -w -r " + + getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath(); + Log.d(LOG_TAG, runCaseCommandStr); + mParser = new InstrumentationResultParser(listener); + + try { + mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser); + } catch (IOException e) { + Log.e(LOG_TAG, e); + listener.testRunFailed(e.toString()); + } + } + + /** + * Requests cancellation of this test run. + */ + public void cancel() { + if (mParser != null) { + mParser.cancel(); + } + } + + /** + * Returns the test class argument. + */ + private String getClassArg() { + return mClassArg; + } + + /** + * Returns the full instrumentation command which specifies the test classes to execute. + * Returns an empty string if no classes were specified. + */ + private String getClassCmd() { + String classArg = getClassArg(); + if (classArg != null) { + return "-e class " + classArg; + } + return ""; + } + + /** + * Returns the full command to enable log only mode - if specified. Otherwise returns an + * empty string. + */ + private String getLogCmd() { + if (mLogOnlyMode) { + return "-e log true"; + } + else { + return ""; + } + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java new file mode 100644 index 0000000..4d3b108 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +/** + * Identifies a parsed instrumentation test + */ +public class TestIdentifier { + + private final String mClassName; + private final String mTestName; + + /** + * Creates a test identifier + * + * @param className fully qualified class name of the test. Cannot be null. + * @param testName name of the test. Cannot be null. + */ + public TestIdentifier(String className, String testName) { + if (className == null || testName == null) { + throw new IllegalArgumentException("className and testName must " + + "be non-null"); + } + mClassName = className; + mTestName = testName; + } + + /** + * Returns the fully qualified class name of the test + */ + public String getClassName() { + return mClassName; + } + + /** + * Returns the name of the test + */ + public String getTestName() { + return mTestName; + } + + /** + * Tests equality by comparing class and method name + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof TestIdentifier)) { + return false; + } + TestIdentifier otherTest = (TestIdentifier)other; + return getClassName().equals(otherTest.getClassName()) && + getTestName().equals(otherTest.getTestName()); + } + + /** + * Generates hashCode based on class and method name. + */ + @Override + public int hashCode() { + return getClassName().hashCode() * 31 + getTestName().hashCode(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java new file mode 100644 index 0000000..8167e5d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.utils; + +/** + * Utility class providing array to int/long conversion for data received from devices through adb. + */ +public final class ArrayHelper { + + /** + * Swaps an unsigned value around, and puts the result in an array that can be sent to a device. + * @param value The value to swap. + * @param dest the destination array + * @param offset the offset in the array where to put the swapped value. + * Array length must be at least offset + 4 + */ + public static void swap32bitsToArray(int value, byte[] dest, int offset) { + dest[offset] = (byte)(value & 0x000000FF); + dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8); + dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16); + dest[offset + 3] = (byte)((value & 0xFF000000) >> 24); + } + + /** + * Reads a signed 32 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * @return the integer read from the array + */ + public static int swap32bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + v |= (((int)value[offset + 2]) & 0x000000FF) << 16; + v |= (((int)value[offset + 3]) & 0x000000FF) << 24; + + return v; + } + + /** + * Reads an unsigned 16 bit integer from an array coming from a device, + * and returns it as an 'int' + * @param value the array containing the 16 bit int (2 byte). + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 2 + * @return the integer read from the array. + */ + public static int swapU16bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + + return v; + } + + /** + * Reads a signed 64 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 8 + * @return the integer read from the array + */ + public static long swap64bitFromArray(byte[] value, int offset) { + long v = 0; + v |= ((long)value[offset]) & 0x00000000000000FFL; + v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8; + v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16; + v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24; + v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32; + v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40; + v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48; + v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56; + + return v; + } +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java new file mode 100644 index 0000000..77d10c1 --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import junit.framework.TestCase; + + +/** + * Tests InstrumentationResultParser. + */ +public class InstrumentationResultParserTest extends TestCase { + + private InstrumentationResultParser mParser; + private VerifyingTestResult mTestResult; + + // static dummy test names to use for validation + private static final String CLASS_NAME = "com.test.FooTest"; + private static final String TEST_NAME = "testFoo"; + private static final String STACK_TRACE = "java.lang.AssertionFailedException"; + + /** + * @param name - test name + */ + public InstrumentationResultParserTest(String name) { + super(name); + } + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestResult = new VerifyingTestResult(); + mParser = new InstrumentationResultParser(mTestResult); + } + + /** + * Tests that the test run started and test start events is sent on first + * bundle received. + */ + public void testTestStarted() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(0, mTestResult.mNumTestsRun); + } + + /** + * Tests that a single successful test execution. + */ + public void testTestSuccess() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addSuccessCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(null, mTestResult.mTestStatus); + } + + /** + * Test basic parsing of failed test case. + */ + public void testTestFailed() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addStackTrace(output); + addFailureCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus); + assertEquals(STACK_TRACE, mTestResult.mTrace); + } + + /** + * Test basic parsing and conversion of time from output. + */ + public void testTimeParsing() { + final String timeString = "Time: 4.9"; + injectTestString(timeString); + assertEquals(4900, mTestResult.mTestTime); + } + + /** + * builds a common test result using TEST_NAME and TEST_CLASS. + */ + private StringBuilder buildCommonResult() { + StringBuilder output = new StringBuilder(); + // add test start bundle + addCommonStatus(output); + addStatusCode(output, "1"); + // add end test bundle, without status + addCommonStatus(output); + return output; + } + + /** + * Adds common status results to the provided output. + */ + private void addCommonStatus(StringBuilder output) { + addStatusKey(output, "stream", "\r\n" + CLASS_NAME); + addStatusKey(output, "test", TEST_NAME); + addStatusKey(output, "class", CLASS_NAME); + addStatusKey(output, "current", "1"); + addStatusKey(output, "numtests", "1"); + addStatusKey(output, "id", "InstrumentationTestRunner"); + } + + /** + * Adds a stack trace status bundle to output. + */ + private void addStackTrace(StringBuilder output) { + addStatusKey(output, "stack", STACK_TRACE); + + } + + /** + * Helper method to add a status key-value bundle. + */ + private void addStatusKey(StringBuilder outputBuilder, String key, + String value) { + outputBuilder.append("INSTRUMENTATION_STATUS: "); + outputBuilder.append(key); + outputBuilder.append('='); + outputBuilder.append(value); + outputBuilder.append("\r\n"); + } + + private void addStartCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "1"); + } + + private void addSuccessCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "0"); + } + + private void addFailureCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "-2"); + } + + private void addStatusCode(StringBuilder outputBuilder, String value) { + outputBuilder.append("INSTRUMENTATION_STATUS_CODE: "); + outputBuilder.append(value); + outputBuilder.append("\r\n"); + } + + /** + * inject a test string into the result parser. + * + * @param result + */ + private void injectTestString(String result) { + byte[] data = result.getBytes(); + mParser.addOutput(data, 0, data.length); + mParser.flush(); + } + + private void assertCommonAttributes() { + assertEquals(CLASS_NAME, mTestResult.mSuiteName); + assertEquals(1, mTestResult.mTestCount); + assertEquals(TEST_NAME, mTestResult.mTestName); + } + + /** + * A specialized test listener that stores a single test events. + */ + private class VerifyingTestResult implements ITestRunListener { + + String mSuiteName; + int mTestCount; + int mNumTestsRun; + String mTestName; + long mTestTime; + TestFailure mTestStatus; + String mTrace; + boolean mStopped; + + VerifyingTestResult() { + mNumTestsRun = 0; + mTestStatus = null; + mStopped = false; + } + + public void testEnded(TestIdentifier test) { + mNumTestsRun++; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + mTestStatus = status; + mTrace = trace; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + } + + public void testRunEnded(long elapsedTime) { + mTestTime = elapsedTime; + + } + + public void testRunStarted(int testCount) { + mTestCount = testCount; + } + + public void testRunStopped(long elapsedTime) { + mTestTime = elapsedTime; + mStopped = true; + } + + public void testStarted(TestIdentifier test) { + mSuiteName = test.getClassName(); + mTestName = test.getTestName(); + } + + public void testRunFailed(String errorMessage) { + // ignored + } + } + +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java new file mode 100644 index 0000000..9acaaf9 --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmlib.testrunner; + +import com.android.ddmlib.Client; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.Device.DeviceState; +import com.android.ddmlib.log.LogReceiver; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.Map; + +/** + * Tests RemoteAndroidTestRunner. + */ +public class RemoteAndroidTestRunnerTest extends TestCase { + + private RemoteAndroidTestRunner mRunner; + private MockDevice mMockDevice; + + private static final String TEST_PACKAGE = "com.test"; + private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner"; + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + mMockDevice = new MockDevice(); + mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice); + } + + /** + * Test the basic case building of the instrumentation runner command with no arguments. + */ + public void testRun() { + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER), + mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with log set. + */ + public void testRunWithLog() { + mRunner.setLogOnly(true); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE, + TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with method set. + */ + public void testRunWithMethod() { + final String className = "FooTest"; + final String testName = "fooTest"; + mRunner.setMethodName(className, testName); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className, + testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with extra args set. + */ + public void testRunWithExtraArgs() { + final String extraArgs = "blah"; + mRunner.setExtraArgs(extraArgs); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs, + TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + + /** + * Assert two strings are equal ignoring whitespace. + */ + private void assertStringsEquals(String str1, String str2) { + String strippedStr1 = str1.replaceAll(" ", ""); + String strippedStr2 = str2.replaceAll(" ", ""); + assertEquals(strippedStr1, strippedStr2); + } + + /** + * A dummy device that does nothing except store the provided executed shell command for + * later retrieval. + */ + private static class MockDevice implements IDevice { + + private String mLastShellCommand; + + /** + * Stores the provided command for later retrieval from getLastShellCommand. + */ + public void executeShellCommand(String command, + IShellOutputReceiver receiver) throws IOException { + mLastShellCommand = command; + } + + /** + * Get the last command provided to executeShellCommand. + */ + public String getLastShellCommand() { + return mLastShellCommand; + } + + public boolean createForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public Client getClient(String applicationName) { + throw new UnsupportedOperationException(); + } + + public String getClientName(int pid) { + throw new UnsupportedOperationException(); + } + + public Client[] getClients() { + throw new UnsupportedOperationException(); + } + + public FileListingService getFileListingService() { + throw new UnsupportedOperationException(); + } + + public Map<String, String> getProperties() { + throw new UnsupportedOperationException(); + } + + public String getProperty(String name) { + throw new UnsupportedOperationException(); + } + + public int getPropertyCount() { + throw new UnsupportedOperationException(); + } + + public RawImage getScreenshot() throws IOException { + throw new UnsupportedOperationException(); + } + + public String getSerialNumber() { + throw new UnsupportedOperationException(); + } + + public DeviceState getState() { + throw new UnsupportedOperationException(); + } + + public SyncService getSyncService() { + throw new UnsupportedOperationException(); + } + + public boolean hasClients() { + throw new UnsupportedOperationException(); + } + + public boolean isBootLoader() { + throw new UnsupportedOperationException(); + } + + public boolean isEmulator() { + throw new UnsupportedOperationException(); + } + + public boolean isOffline() { + throw new UnsupportedOperationException(); + } + + public boolean isOnline() { + throw new UnsupportedOperationException(); + } + + public boolean removeForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public void runEventLogService(LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public void runLogService(String logname, LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public String getAvdName() { + return ""; + } + + } + + /** + * An empty implementation of ITestRunListener. + */ + private static class EmptyListener implements ITestRunListener { + + public void testEnded(TestIdentifier test) { + // ignore + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + // ignore + } + + public void testRunEnded(long elapsedTime) { + // ignore + } + + public void testRunFailed(String errorMessage) { + // ignore + } + + public void testRunStarted(int testCount) { + // ignore + } + + public void testRunStopped(long elapsedTime) { + // ignore + } + + public void testStarted(TestIdentifier test) { + // ignore + } + + } +} diff --git a/ddms/libs/ddmuilib/.classpath b/ddms/libs/ddmuilib/.classpath new file mode 100644 index 0000000..ce7e7f0 --- /dev/null +++ b/ddms/libs/ddmuilib/.classpath @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="Makefile|resources" kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/> + <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/> + <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_JFREECHART"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/libs/ddmuilib/.project b/ddms/libs/ddmuilib/.project new file mode 100644 index 0000000..29cb2f2 --- /dev/null +++ b/ddms/libs/ddmuilib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddmuilib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/libs/ddmuilib/Android.mk b/ddms/libs/ddmuilib/Android.mk new file mode 100644 index 0000000..7059e5e --- /dev/null +++ b/ddms/libs/ddmuilib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +DDMUILIB_LOCAL_DIR := $(call my-dir) +include $(DDMUILIB_LOCAL_DIR)/src/Android.mk diff --git a/ddms/libs/ddmuilib/README b/ddms/libs/ddmuilib/README new file mode 100644 index 0000000..d66b84a --- /dev/null +++ b/ddms/libs/ddmuilib/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for ddmuilib. + +ddmuilib requires SWT to compile. + +SWT is available in the depot under prebuild/<platform>/swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at prebuild/<platform>/swt.
\ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/Android.mk b/ddms/libs/ddmuilib/src/Android.mk new file mode 100644 index 0000000..acbda44 --- /dev/null +++ b/ddms/libs/ddmuilib/src/Android.mk @@ -0,0 +1,22 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAVA_LIBRARIES := \ + ddmlib \ + swt \ + org.eclipse.jface_3.2.0.I20060605-1400 \ + org.eclipse.equinox.common_3.2.0.v20060603 \ + org.eclipse.core.commands_3.2.0.I20060605-1400 \ + jcommon-1.0.12 \ + jfreechart-1.0.9 \ + jfreechart-1.0.9-swt + +LOCAL_MODULE := ddmuilib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 0000000..a2f12d5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.*; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; + +/** + * Represents an addr2line process to get filename/method information from a + * memory address.<br> + * Each process can only handle one library, which should be provided when + * creating a new process.<br> + * <br> + * The processes take some time to load as they need to parse the library files. + * For this reason, processes cannot be manually started. Instead the class + * keeps an internal list of processes and one asks for a process for a specific + * library, using <code>getProcess(String library)<code>.<br></br> + * Internally, the processes are started in pipe mode to be able to query them + * with multiple addresses. + */ +public class Addr2Line { + + /** + * Loaded processes list. This is also used as a locking object for any + * methods dealing with starting/stopping/creating processes/querying for + * method. + */ + private static final HashMap<String, Addr2Line> sProcessCache = + new HashMap<String, Addr2Line>(); + + /** + * byte array representing a carriage return. Used to push addresses in the + * process pipes. + */ + private static final byte[] sCrLf = { + '\n' + }; + + /** Path to the library */ + private String mLibrary; + + /** the command line process */ + private Process mProcess; + + /** buffer to read the result of the command line process from */ + private BufferedReader mResultReader; + + /** + * output stream to provide new addresses to decode to the command line + * process + */ + private BufferedOutputStream mAddressWriter; + + /** + * Returns the instance of a Addr2Line process for the specified library. + * <br>The library should be in a format that makes<br> + * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file. + * + * @param library the library in which to look for addresses. + * @return a new Addr2Line object representing a started process, ready to + * be queried for addresses. If any error happened when launching a + * new process, <code>null</code> will be returned. + */ + public static Addr2Line getProcess(final String library) { + // synchronize around the hashmap object + if (library != null) { + synchronized (sProcessCache) { + // look for an existing process + Addr2Line process = sProcessCache.get(library); + + // if we don't find one, we create it + if (process == null) { + process = new Addr2Line(library); + + // then we start it + boolean status = process.start(); + + if (status) { + // if starting the process worked, then we add it to the + // list. + sProcessCache.put(library, process); + } else { + // otherwise we just drop the object, to return null + process = null; + } + } + // return the process + return process; + } + } + return null; + } + + /** + * Construct the object with a library name. + * <br>The library should be in a format that makes<br> + * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file. + * + * @param library the library in which to look for address. + */ + private Addr2Line(final String library) { + mLibrary = library; + } + + /** + * Starts the command line process. + * + * @return true if the process was started, false if it failed to start, or + * if there was any other errors. + */ + private boolean start() { + // because this is only called from getProcess() we know we don't need + // to synchronize this code. + + // get the output directory. + String symbols = DdmUiPreferences.getSymbolDirectory(); + + // build the command line + String[] command = new String[5]; + command[0] = DdmUiPreferences.getAddr2Line(); + command[1] = "-C"; + command[2] = "-f"; + command[3] = "-e"; + command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so"); + + try { + // attempt to start the process + mProcess = Runtime.getRuntime().exec(command); + + if (mProcess != null) { + // get the result reader + InputStreamReader is = new InputStreamReader(mProcess + .getInputStream()); + mResultReader = new BufferedReader(is); + + // get the outstream to write the addresses + mAddressWriter = new BufferedOutputStream(mProcess + .getOutputStream()); + + // check our streams are here + if (mResultReader == null || mAddressWriter == null) { + // not here? stop the process and return false; + mProcess.destroy(); + mProcess = null; + return false; + } + + // return a success + return true; + } + + } catch (IOException e) { + // log the error + String msg = String.format( + "Error while trying to start %1$s process for library %2$s", + DdmUiPreferences.getAddr2Line(), mLibrary); + Log.e("ddm-Addr2Line", msg); + + // drop the process just in case + if (mProcess != null) { + mProcess.destroy(); + mProcess = null; + } + } + + // we can be here either cause the allocation of mProcess failed, or we + // caught an exception + return false; + } + + /** + * Stops the command line process. + */ + public void stop() { + synchronized (sProcessCache) { + if (mProcess != null) { + // remove the process from the list + sProcessCache.remove(mLibrary); + + // then stops the process + mProcess.destroy(); + + // set the reference to null. + // this allows to make sure another thread calling getAddress() + // will not query a stopped thread + mProcess = null; + } + } + } + + /** + * Stops all current running processes. + */ + public static void stopAll() { + // because of concurrent access (and our use of HashMap.values()), we + // can't rely on the synchronized inside stop(). We need to put one + // around the whole loop. + synchronized (sProcessCache) { + // just a basic loop on all the values in the hashmap and call to + // stop(); + Collection<Addr2Line> col = sProcessCache.values(); + for (Addr2Line a2l : col) { + a2l.stop(); + } + } + } + + /** + * Looks up an address and returns method name, source file name, and line + * number. + * + * @param addr the address to look up + * @return a BacktraceInfo object containing the method/filename/linenumber + * or null if the process we stopped before the query could be + * processed, or if an IO exception happened. + */ + public NativeStackCallInfo getAddress(long addr) { + // even though we don't access the hashmap object, we need to + // synchronized on it to prevent + // another thread from stopping the process we're going to query. + synchronized (sProcessCache) { + // check the process is still alive/allocated + if (mProcess != null) { + // prepare to the write the address to the output buffer. + + // first, conversion to a string containing the hex value. + String tmp = Long.toString(addr, 16); + + try { + // write the address to the buffer + mAddressWriter.write(tmp.getBytes()); + + // add CR-LF + mAddressWriter.write(sCrLf); + + // flush it all. + mAddressWriter.flush(); + + // read the result. We need to read 2 lines + String method = mResultReader.readLine(); + String source = mResultReader.readLine(); + + // make the backtrace object and return it + if (method != null && source != null) { + return new NativeStackCallInfo(mLibrary, method, source); + } + } catch (IOException e) { + // log the error + Log.e("ddms", + "Error while trying to get information for addr: " + + tmp + " in library: " + mLibrary); + // we'll return null later + } + } + } + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 0000000..45d45ff --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.AllocationInfo; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +/** + * Base class for our information panels. + */ +public class AllocationPanel extends TablePanel { + + private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$ + + private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$ + + private Composite mAllocationBase; + private Table mAllocationTable; + private TableViewer mAllocationViewer; + + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + private Button mEnableButton; + private Button mRequestButton; + + /** + * Content Provider to display the allocations of a client. + * Expected input is a {@link Client} object, elements used in the table are of type + * {@link AllocationInfo}. + */ + private static class AllocationContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations(); + if (allocs != null) { + return allocs; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be + * of type {@link AllocationInfo}. + */ + private static class AllocationLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof AllocationInfo) { + AllocationInfo alloc = (AllocationInfo)element; + switch (columnIndex) { + case 0: + return Integer.toString(alloc.getSize()); + case 1: + return alloc.getAllocatedClass(); + case 2: + return Short.toString(alloc.getThreadId()); + case 3: + StackTraceElement[] traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getClassName(); + } + break; + case 4: + traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getMethodName(); + } + break; + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // base composite for selected client with enabled thread update. + mAllocationBase = new Composite(parent, SWT.NONE); + mAllocationBase.setLayout(new FormLayout()); + + // table above the sash + Composite topParent = new Composite(mAllocationBase, SWT.NONE); + topParent.setLayout(new GridLayout(2, false)); + + mEnableButton = new Button(topParent, SWT.PUSH); + mEnableButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client current = getCurrentClient(); + int status = current.getClientData().getAllocationStatus(); + if (status == ClientData.ALLOCATION_TRACKING_ON) { + current.enableAllocationTracker(false); + } else { + current.enableAllocationTracker(true); + } + current.requestAllocationStatus(); + } + }); + + mRequestButton = new Button(topParent, SWT.PUSH); + mRequestButton.setText("Get Allocations"); + mRequestButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + getCurrentClient().requestAllocationDetails(); + } + }); + + setUpButtons(false /* enabled */, ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */); + + mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION); + GridData gridData; + mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH)); + gridData.horizontalSpan = 2; + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocation Size", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_ALLOC_COL_SIZE, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated Class", + SWT.LEFT, + "Allocated Class", //$NON-NLS-1$ + PREFS_ALLOC_COL_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Thread Id", + SWT.LEFT, + "999", //$NON-NLS-1$ + PREFS_ALLOC_COL_THREAD, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_METHOD, store); + + mAllocationViewer = new TableViewer(mAllocationTable); + mAllocationViewer.setContentProvider(new AllocationContentProvider()); + mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); + + mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection()); + updateAllocationStackTrace(selectedAlloc); + } + }); + + // the separating sash + final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topParent.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_ALLOC_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mAllocationBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_ALLOC_SASH, e.y); + mAllocationBase.layout(); + } + } + }); + + return mAllocationBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mAllocationTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + mAllocationViewer.refresh(); + updateAllocationStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + setUpButtons(true, client.getClientData().getAllocationStatus()); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mAllocationTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + mStackTracePanel.setViewerInput(null); // always empty on client selection change. + + if (client != null) { + setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus()); + } else { + setUpButtons(false /* enabled */, + ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */); + } + + mAllocationViewer.setInput(client); + } + + /** + * Updates the stack call of the currently selected thread. + * <p/> + * This <b>must</b> be called from the UI thread. + */ + private void updateAllocationStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + AllocationInfo selectedAlloc = getAllocationSelection(null); + + if (selectedAlloc != null) { + updateAllocationStackTrace(selectedAlloc); + } else { + updateAllocationStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified allocation. If <code>null</code> the UI is emptied + * of current data. + * @param thread + */ + private void updateAllocationStackTrace(AllocationInfo alloc) { + mStackTracePanel.setViewerInput(alloc); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Returns the current allocation selection or <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this + * selection is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + private AllocationInfo getAllocationSelection(ISelection selection) { + if (selection == null) { + selection = mAllocationViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof AllocationInfo) { + return (AllocationInfo)object; + } + } + + return null; + } + + /** + * + * @param enabled + * @param trackingStatus + */ + private void setUpButtons(boolean enabled, int trackingStatus) { + if (enabled) { + switch (trackingStatus) { + case ClientData.ALLOCATION_TRACKING_UNKNOWN: + mEnableButton.setText("?"); + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + break; + case ClientData.ALLOCATION_TRACKING_OFF: + mEnableButton.setText("Start Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(false); + break; + case ClientData.ALLOCATION_TRACKING_ON: + mEnableButton.setText("Stop Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(true); + break; + } + } else { + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + mEnableButton.setText("Start Tracking"); + } + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java new file mode 100644 index 0000000..0ed4c95 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Log; + +/** + * base background thread class. The class provides a synchronous quit method + * which sets a quitting flag to true. Inheriting classes should regularly test + * this flag with <code>isQuitting()</code> and should finish if the flag is + * true. + */ +public abstract class BackgroundThread extends Thread { + private boolean mQuit = false; + + /** + * Tell the thread to exit. This is usually called from the UI thread. The + * call is synchronous and will only return once the thread has terminated + * itself. + */ + public final void quit() { + mQuit = true; + Log.d("ddms", "Waiting for BackgroundThread to quit"); + try { + this.join(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + /** returns if the thread was asked to quit. */ + protected final boolean isQuitting() { + return mQuit; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java new file mode 100644 index 0000000..3e66ea5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.HeapSegment; +import com.android.ddmlib.ClientData.HeapData; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + + +/** + * Base Panel for heap panels. + */ +public abstract class BaseHeapPanel extends TablePanel { + + /** store the processed heap segment, so that we don't recompute Image for nothing */ + protected byte[] mProcessedHeapData; + private Map<Integer, ArrayList<HeapSegmentElement>> mHeapMap; + + /** + * Serialize the heap data into an array. The resulting array is available through + * <code>getSerializedData()</code>. + * @param heapData The heap data to serialize + * @return true if the data changed. + */ + protected boolean serializeHeapData(HeapData heapData) { + Collection<HeapSegment> heapSegments; + + // Atomically get and clear the heap data. + synchronized (heapData) { + // get the segments + heapSegments = heapData.getHeapSegments(); + + + if (heapSegments != null) { + // if they are not null, we never processed them. + // Before we process then, we drop them from the HeapData + heapData.clearHeapData(); + + // process them into a linear byte[] + doSerializeHeapData(heapSegments); + heapData.setProcessedHeapData(mProcessedHeapData); + heapData.setProcessedHeapMap(mHeapMap); + + } else { + // the heap segments are null. Let see if the heapData contains a + // list that is already processed. + + byte[] pixData = heapData.getProcessedHeapData(); + + // and compare it to the one we currently have in the panel. + if (pixData == mProcessedHeapData) { + // looks like its the same + return false; + } else { + mProcessedHeapData = pixData; + } + + Map<Integer, ArrayList<HeapSegmentElement>> heapMap = + heapData.getProcessedHeapMap(); + mHeapMap = heapMap; + } + } + + return true; + } + + /** + * Returns the serialized heap data + */ + protected byte[] getSerializedData() { + return mProcessedHeapData; + } + + /** + * Processes and serialize the heapData. + * <p/> + * The resulting serialized array is {@link #mProcessedHeapData}. + * <p/> + * the resulting map is {@link #mHeapMap}. + * @param heapData the collection of {@link HeapSegment} that forms the heap data. + */ + private void doSerializeHeapData(Collection<HeapSegment> heapData) { + mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>(); + + Iterator<HeapSegment> iterator; + ByteArrayOutputStream out; + + out = new ByteArrayOutputStream(4 * 1024); + + iterator = heapData.iterator(); + while (iterator.hasNext()) { + HeapSegment hs = iterator.next(); + + HeapSegmentElement e = null; + while (true) { + int v; + + e = hs.getNextElement(null); + if (e == null) { + break; + } + + if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) { + v = 1; + } else { + v = e.getKind() + 2; + } + + // put the element in the map + ArrayList<HeapSegmentElement> elementList = mHeapMap.get(v); + if (elementList == null) { + elementList = new ArrayList<HeapSegmentElement>(); + mHeapMap.put(v, elementList); + } + elementList.add(e); + + + int len = e.getLength() / 8; + while (len > 0) { + out.write(v); + --len; + } + } + } + mProcessedHeapData = out.toByteArray(); + + // sort the segment element in the heap info. + Collection<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values(); + for (ArrayList<HeapSegmentElement> elementList : elementLists) { + Collections.sort(elementList); + } + } + + /** + * Creates a linear image of the heap data. + * @param pixData + * @param h + * @param palette + * @return + */ + protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) { + int w = pixData.length / h; + if (pixData.length % h != 0) { + w++; + } + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, palette); + + int x = 0; + int y = 0; + for (byte b : pixData) { + if (b >= 0) { + id.setPixel(x, y, b); + } + + y++; + if (y >= h) { + y = 0; + x++; + } + } + + return id; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 0000000..a711933 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +public abstract class ClientDisplayPanel extends SelectionDependentPanel + implements IClientChangeListener { + + @Override + protected void postCreation() { + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeClientChangeListener(this); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 0000000..f832a4e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.preference.IPreferenceStore; + +/** + * Preference entry point for ddmuilib. Allows the lib to access a preference + * store (org.eclipse.jface.preference.IPreferenceStore) defined by the + * application that includes the lib. + */ +public final class DdmUiPreferences { + + public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds + + private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL; + + private static IPreferenceStore mStore; + + private static String sSymbolLocation =""; //$NON-NLS-1$ + private static String sAddr2LineLocation =""; //$NON-NLS-1$ + private static String sTraceviewLocation =""; //$NON-NLS-1$ + + public static void setStore(IPreferenceStore store) { + mStore = store; + } + + public static IPreferenceStore getStore() { + return mStore; + } + + public static int getThreadRefreshInterval() { + return sThreadRefreshInterval; + } + + public static void setThreadRefreshInterval(int port) { + sThreadRefreshInterval = port; + } + + static String getSymbolDirectory() { + return sSymbolLocation; + } + + public static void setSymbolsLocation(String location) { + sSymbolLocation = location; + } + + static String getAddr2Line() { + return sAddr2LineLocation; + } + + public static void setAddr2LineLocation(String location) { + sAddr2LineLocation = location; + } + + public static String getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 0000000..81b757e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,744 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.Device; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Device.DeviceState; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.util.ArrayList; + +/** + * A display of both the devices and their clients. + */ +public final class DevicePanel extends Panel implements IDebugBridgeChangeListener, + IDeviceChangeListener, IClientChangeListener { + + private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$ + + private final static int DEVICE_COL_SERIAL = 0; + private final static int DEVICE_COL_STATE = 1; + // col 2, 3 not used. + private final static int DEVICE_COL_BUILD = 4; + + private final static int CLIENT_COL_NAME = 0; + private final static int CLIENT_COL_PID = 1; + private final static int CLIENT_COL_THREAD = 2; + private final static int CLIENT_COL_HEAP = 3; + private final static int CLIENT_COL_PORT = 4; + + public final static int ICON_WIDTH = 16; + public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$ + public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$ + public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$ + public final static String ICON_GC = "gc.png"; //$NON-NLS-1$ + + private Device mCurrentDevice; + private Client mCurrentClient; + + private Tree mTree; + private TreeViewer mTreeViewer; + + private Image mDeviceImage; + private Image mEmulatorImage; + + private Image mThreadImage; + private Image mHeapImage; + private Image mWaitingImage; + private Image mDebuggerImage; + private Image mDebugErrorImage; + + private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>(); + + private final ArrayList<Device> mDevicesToExpand = new ArrayList<Device>(); + + private IImageLoader mLoader; + + private boolean mAdvancedPortSupport; + + /** + * A Content provider for the {@link TreeViewer}. + * <p/> + * The input is a {@link AndroidDebugBridge}. First level elements are {@link Device} objects, + * and second level elements are {@link Client} object. + */ + private class ContentProvider implements ITreeContentProvider { + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof Device) { + return ((Device)parentElement).getClients(); + } + return new Object[0]; + } + + public Object getParent(Object element) { + if (element instanceof Client) { + return ((Client)element).getDevice(); + } + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof Device) { + return ((Device)element).hasClients(); + } + + // Clients never have children. + return false; + } + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return ((AndroidDebugBridge)inputElement).getDevices(); + } + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides + * labels and images for {@link Device} and {@link Client} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == DEVICE_COL_SERIAL && element instanceof Device) { + Device device = (Device)element; + if (device.isEmulator()) { + return mEmulatorImage; + } + + return mDeviceImage; + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + switch (cd.getDebuggerConnectionStatus()) { + case ClientData.DEBUGGER_DEFAULT: + return null; + case ClientData.DEBUGGER_WAITING: + return mWaitingImage; + case ClientData.DEBUGGER_ATTACHED: + return mDebuggerImage; + case ClientData.DEBUGGER_ERROR: + return mDebugErrorImage; + } + return null; + case CLIENT_COL_THREAD: + if (client.isThreadUpdateEnabled()) { + return mThreadImage; + } + return null; + case CLIENT_COL_HEAP: + if (client.isHeapUpdateEnabled()) { + return mHeapImage; + } + return null; + } + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Device) { + Device device = (Device)element; + switch (columnIndex) { + case DEVICE_COL_SERIAL: + return device.getSerialNumber(); + case DEVICE_COL_STATE: + return getStateString(device); + case DEVICE_COL_BUILD: { + String version = device.getProperty(Device.PROP_BUILD_VERSION); + if (version != null) { + String debuggable = device.getProperty(Device.PROP_DEBUGGABLE); + if (device.isEmulator()) { + String avdName = device.getAvdName(); + if (avdName == null) { + avdName = "?"; // the device is probably not online yet, so + // we don't know its AVD name just yet. + } + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s [%2$s, debug]", avdName, + version); + } else { + return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$ + } + } else { + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s, debug", version); + } else { + return String.format("%1$s", version); //$NON-NLS-1$ + } + } + } else { + return "unknown"; + } + } + } + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + String name = cd.getClientDescription(); + if (name != null) { + return name; + } + return "?"; + case CLIENT_COL_PID: + return Integer.toString(cd.getPid()); + case CLIENT_COL_PORT: + if (mAdvancedPortSupport) { + int port = client.getDebuggerListenPort(); + String portString = "?"; + if (port != 0) { + portString = Integer.toString(port); + } + if (client.isSelectedClient()) { + return String.format("%1$s / %2$d", portString, //$NON-NLS-1$ + DdmPreferences.getSelectedDebugPort()); + } + + return portString; + } + } + } + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide methods that deals + * with {@link Device} and {@link Client} selection changes coming from the ui. + */ + public interface IUiSelectionListener { + /** + * Sent when a new {@link Device} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public void selectionChanged(Device selectedDevice, Client selectedClient); + } + + /** + * Creates the {@link DevicePanel} object. + * @param loader + * @param advancedPortSupport if true the device panel will add support for selected client port + * and display the ports in the ui. + */ + public DevicePanel(IImageLoader loader, boolean advancedPortSupport) { + mLoader = loader; + mAdvancedPortSupport = advancedPortSupport; + } + + public void addSelectionListener(IUiSelectionListener listener) { + mListeners.add(listener); + } + + public void removeSelectionListener(IUiSelectionListener listener) { + mListeners.remove(listener); + } + + @Override + protected Control createControl(Composite parent) { + loadImages(parent.getDisplay(), mLoader); + + parent.setLayout(new FillLayout()); + + // create the tree and its column + mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); + mTree.setHeaderVisible(true); + mTree.setLinesVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "com.android.home", //$NON-NLS-1$ + PREFS_COL_NAME_SERIAL, store); + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "Offline", //$NON-NLS-1$ + PREFS_COL_PID_STATE, store); + + TreeColumn col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "9999-9999", //$NON-NLS-1$ + PREFS_COL_PORT_BUILD, store); + + // create the tree viewer + mTreeViewer = new TreeViewer(mTree); + + // make the device auto expanded. + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + // set up the content and label providers. + mTreeViewer.setContentProvider(new ContentProvider()); + mTreeViewer.setLabelProvider(new LabelProvider()); + + mTree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + notifyListeners(); + } + }); + + return mTree; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + @Override + protected void postCreation() { + // ask for notification of changes in AndroidDebugBridge (a new one is created when + // adb is restarted from a different location), Device and Client objects. + AndroidDebugBridge.addDebugBridgeChangeListener(this); + AndroidDebugBridge.addDeviceChangeListener(this); + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + + /** + * Returns the selected {@link Client}. May be null. + */ + public Client getSelectedClient() { + return mCurrentClient; + } + + /** + * Returns the selected {@link Device}. If a {@link Client} is selected, it returns the + * Device object containing the client. + */ + public Device getSelectedDevice() { + return mCurrentDevice; + } + + /** + * Kills the selected {@link Client} by sending its VM a halt command. + */ + public void killSelectedClient() { + if (mCurrentClient != null) { + Client client = mCurrentClient; + + // reset the selection to the device. + TreePath treePath = new TreePath(new Object[] { mCurrentDevice }); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + client.kill(); + } + } + + /** + * Forces a GC on the selected {@link Client}. + */ + public void forceGcOnSelectedClient() { + if (mCurrentClient != null) { + mCurrentClient.executeGarbageCollector(); + } + } + + public void setEnabledHeapOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setHeapUpdateEnabled(enable); + } + } + + public void setEnabledThreadOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setThreadUpdateEnabled(enable); + } + } + + /** + * Sent when a new {@link AndroidDebugBridge} is started. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + * + * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge) + */ + public void bridgeChanged(final AndroidDebugBridge bridge) { + if (mTree.isDisposed() == false) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // set up the data source. + mTreeViewer.setInput(bridge); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + // all current devices are obsolete + synchronized (mDevicesToExpand) { + mDevicesToExpand.clear(); + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(Device) + */ + public void deviceConnected(Device device) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh all + mTreeViewer.refresh(); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + + // if it doesn't have clients yet, it'll need to be manually expanded when it gets them. + if (device.hasClients() == false) { + synchronized (mDevicesToExpand) { + mDevicesToExpand.add(device); + } + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(Device) + */ + public void deviceDisconnected(Device device) { + deviceConnected(device); + + // just in case, we remove it from the list of devices to expand. + synchronized (mDevicesToExpand) { + mDevicesToExpand.remove(device); + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(Device) + */ + public void deviceChanged(final Device device, int changeMask) { + boolean expand = false; + synchronized (mDevicesToExpand) { + int index = mDevicesToExpand.indexOf(device); + if (device.hasClients() && index != -1) { + mDevicesToExpand.remove(index); + expand = true; + } + } + + final boolean finalExpand = expand; + + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // look if the current device is selected. This is done in case the current + // client of this particular device was killed. In this case, we'll need to + // manually reselect the device. + + Device selectedDevice = getSelectedDevice(); + + // refresh the device + mTreeViewer.refresh(device); + + // if the selected device was the changed device and the new selection is + // empty, we reselect the device. + if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) { + mTreeViewer.setSelection(new TreeSelection(new TreePath( + new Object[] { device }))); + } + + // notify the listener of a possible selection change. + notifyListeners(); + + if (finalExpand) { + mTreeViewer.setExpandedState(device, true); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, final int changeMask) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh the client + mTreeViewer.refresh(client); + + if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) == + Client.CHANGE_DEBUGGER_INTEREST && + client.getClientData().getDebuggerConnectionStatus() == + ClientData.DEBUGGER_WAITING) { + // make sure the device is expanded. Normally the setSelection below + // will auto expand, but the children of device may not already exist + // at this time. Forcing an expand will make the TreeViewer create them. + Device device = client.getDevice(); + if (mTreeViewer.getExpandedState(device) == false) { + mTreeViewer.setExpandedState(device, true); + } + + // create and set the selection + TreePath treePath = new TreePath(new Object[] { device, client}); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + if (mAdvancedPortSupport) { + client.setAsSelectedClient(); + } + + // notify the listener of a possible selection change. + notifyListeners(device, client); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + private void loadImages(Display display, IImageLoader loader) { + if (mDeviceImage == null) { + mDeviceImage = ImageHelper.loadImage(loader, display, "device.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + if (mEmulatorImage == null) { + mEmulatorImage = ImageHelper.loadImage(loader, display, + "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mThreadImage == null) { + mThreadImage = ImageHelper.loadImage(loader, display, ICON_THREAD, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + if (mHeapImage == null) { + mHeapImage = ImageHelper.loadImage(loader, display, ICON_HEAP, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mWaitingImage == null) { + mWaitingImage = ImageHelper.loadImage(loader, display, + "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + if (mDebuggerImage == null) { + mDebuggerImage = ImageHelper.loadImage(loader, display, + "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_GREEN)); + } + if (mDebugErrorImage == null) { + mDebugErrorImage = ImageHelper.loadImage(loader, display, + "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + } + + /** + * Returns a display string representing the state of the device. + * @param d the device + */ + private static String getStateString(Device d) { + DeviceState deviceState = d.getState(); + if (deviceState == DeviceState.ONLINE) { + return "Online"; + } else if (deviceState == DeviceState.OFFLINE) { + return "Offline"; + } else if (deviceState == DeviceState.BOOTLOADER) { + return "Bootloader"; + } + + return "??"; + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mTree.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + } + + private void notifyListeners() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + Client client = null; + Device device = null; + + if (items.length == 1) { + Object object = items[0].getData(); + if (object instanceof Client) { + client = (Client)object; + device = client.getDevice(); + } else if (object instanceof Device) { + device = (Device)object; + } + } + + notifyListeners(device, client); + } + + private void notifyListeners(Device selectedDevice, Client selectedClient) { + if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) { + mCurrentDevice = selectedDevice; + mCurrentClient = selectedClient; + + for (IUiSelectionListener listener : mListeners) { + // notify the listener with a try/catch-all to make sure this thread won't die + // because of an uncaught exception before all the listeners were notified. + try { + listener.selectionChanged(selectedDevice, selectedClient); + } catch (Exception e) { + } + } + } + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 0000000..5583760 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1454 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Device; +import com.android.ddmlib.EmulatorConsole; +import com.android.ddmlib.EmulatorConsole.GsmMode; +import com.android.ddmlib.EmulatorConsole.GsmStatus; +import com.android.ddmlib.EmulatorConsole.NetworkStatus; +import com.android.ddmuilib.location.CoordinateControls; +import com.android.ddmuilib.location.GpxParser; +import com.android.ddmuilib.location.KmlParser; +import com.android.ddmuilib.location.TrackContentProvider; +import com.android.ddmuilib.location.TrackLabelProvider; +import com.android.ddmuilib.location.TrackPoint; +import com.android.ddmuilib.location.WayPoint; +import com.android.ddmuilib.location.WayPointContentProvider; +import com.android.ddmuilib.location.WayPointLabelProvider; +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; + +/** + * Panel to control the emulator using EmulatorConsole objects. + */ +public class EmulatorControlPanel extends SelectionDependentPanel { + + // default location: Patio outside Charlie's + private final static double DEFAULT_LONGITUDE = -122.084095; + private final static double DEFAULT_LATITUDE = 37.422006; + + private final static String SPEED_FORMAT = "Speed: %1$dX"; + + + /** + * Map between the display gsm mode and the internal tag used by the display. + */ + private final static String[][] GSM_MODES = new String[][] { + { "unregistered", GsmMode.UNREGISTERED.getTag() }, + { "home", GsmMode.HOME.getTag() }, + { "roaming", GsmMode.ROAMING.getTag() }, + { "searching", GsmMode.SEARCHING.getTag() }, + { "denied", GsmMode.DENIED.getTag() }, + }; + + private final static String[] NETWORK_SPEEDS = new String[] { + "Full", + "GSM", + "HSCSD", + "GPRS", + "EDGE", + "UMTS", + "HSDPA", + }; + + private final static String[] NETWORK_LATENCIES = new String[] { + "None", + "GPRS", + "EDGE", + "UMTS", + }; + + private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 }; + + private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$ + + private IImageLoader mImageLoader; + + private EmulatorConsole mEmulatorConsole; + + private Composite mParent; + + private Label mVoiceLabel; + private Combo mVoiceMode; + private Label mDataLabel; + private Combo mDataMode; + private Label mSpeedLabel; + private Combo mNetworkSpeed; + private Label mLatencyLabel; + private Combo mNetworkLatency; + + private Label mNumberLabel; + private Text mPhoneNumber; + + private Button mVoiceButton; + private Button mSmsButton; + + private Label mMessageLabel; + private Text mSmsMessage; + + private Button mCallButton; + private Button mCancelButton; + + private TabFolder mLocationFolders; + + private Button mDecimalButton; + private Button mSexagesimalButton; + private CoordinateControls mLongitudeControls; + private CoordinateControls mLatitudeControls; + private Button mGpxUploadButton; + private Table mGpxWayPointTable; + private Table mGpxTrackTable; + private Button mKmlUploadButton; + private Table mKmlWayPointTable; + + private Button mPlayGpxButton; + private Button mGpxBackwardButton; + private Button mGpxForwardButton; + private Button mGpxSpeedButton; + private Button mPlayKmlButton; + private Button mKmlBackwardButton; + private Button mKmlForwardButton; + private Button mKmlSpeedButton; + + private Image mPlayImage; + private Image mPauseImage; + + private Thread mPlayingThread; + private boolean mPlayingTrack; + private int mPlayDirection = 1; + private int mSpeed; + private int mSpeedIndex; + + private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Button b = (Button)e.getSource(); + if (b.getSelection() == false) { + // basically the button was unselected, which we don't allow. + // so we reselect it. + b.setSelection(true); + return; + } + + // now handle selection change. + if (b == mGpxForwardButton || b == mKmlForwardButton) { + mGpxBackwardButton.setSelection(false); + mGpxForwardButton.setSelection(true); + mKmlBackwardButton.setSelection(false); + mKmlForwardButton.setSelection(true); + mPlayDirection = 1; + + } else { + mGpxBackwardButton.setSelection(true); + mGpxForwardButton.setSelection(false); + mKmlBackwardButton.setSelection(true); + mKmlForwardButton.setSelection(false); + mPlayDirection = -1; + } + } + }; + + private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxPlayControls.pack(); + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlPlayControls.pack(); + + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + }; + private Composite mKmlPlayControls; + private Composite mGpxPlayControls; + + + public EmulatorControlPanel(IImageLoader imageLoader) { + mImageLoader = imageLoader; + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + handleNewDevice(getCurrentDevice()); + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + // pass + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + + final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL); + scollingParent.setExpandVertical(true); + scollingParent.setExpandHorizontal(true); + scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH)); + + final Composite top = new Composite(scollingParent, SWT.NONE); + scollingParent.setContent(top); + top.setLayout(new GridLayout(1, false)); + + // set the resize for the scrolling to work (why isn't that done automatically?!?) + scollingParent.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + } + }); + + createRadioControls(top); + + createCallControls(top); + + createLocationControls(top); + + doEnable(false); + + top.layout(); + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + + return scollingParent; + } + + /** + * Create Radio (on/off/roaming, for voice/data) controls. + * @param top + */ + private void createRadioControls(final Composite top) { + Group g1 = new Group(top, SWT.NONE); + g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g1.setLayout(new GridLayout(2, false)); + g1.setText("Telephony Status"); + + // the inside of the group is 2 composite so that all the column of the controls (mainly + // combos) have the same width, while not taking the whole screen width + Composite insideGroup = new Composite(g1, SWT.NONE); + GridLayout gl = new GridLayout(4, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + insideGroup.setLayout(gl); + + mVoiceLabel = new Label(insideGroup, SWT.NONE); + mVoiceLabel.setText("Voice:"); + mVoiceLabel.setAlignment(SWT.RIGHT); + + mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY); + mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mVoiceMode.add(mode[0]); + } + mVoiceMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setVoiceMode(mVoiceMode.getSelectionIndex()); + } + }); + + mSpeedLabel = new Label(insideGroup, SWT.NONE); + mSpeedLabel.setText("Speed:"); + mSpeedLabel.setAlignment(SWT.RIGHT); + + mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_SPEEDS) { + mNetworkSpeed.add(mode); + } + mNetworkSpeed.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkSpeed(mNetworkSpeed.getSelectionIndex()); + } + }); + + mDataLabel = new Label(insideGroup, SWT.NONE); + mDataLabel.setText("Data:"); + mDataLabel.setAlignment(SWT.RIGHT); + + mDataMode = new Combo(insideGroup, SWT.READ_ONLY); + mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mDataMode.add(mode[0]); + } + mDataMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setDataMode(mDataMode.getSelectionIndex()); + } + }); + + mLatencyLabel = new Label(insideGroup, SWT.NONE); + mLatencyLabel.setText("Latency:"); + mLatencyLabel.setAlignment(SWT.RIGHT); + + mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_LATENCIES) { + mNetworkLatency.add(mode); + } + mNetworkLatency.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkLatency(mNetworkLatency.getSelectionIndex()); + } + }); + + // now an empty label to take the rest of the width of the group + Label l = new Label(g1, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + } + + /** + * Create Voice/SMS call/hang up controls + * @param top + */ + private void createCallControls(final Composite top) { + GridLayout gl; + Group g2 = new Group(top, SWT.NONE); + g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g2.setLayout(new GridLayout(1, false)); + g2.setText("Telephony Actions"); + + // horizontal composite for label + text field + Composite phoneComp = new Composite(g2, SWT.NONE); + phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH)); + gl = new GridLayout(2, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + phoneComp.setLayout(gl); + + mNumberLabel = new Label(phoneComp, SWT.NONE); + mNumberLabel.setText("Incoming number:"); + + mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPhoneNumber.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + // Reenable the widgets based on the content of the text. + // doEnable checks the validity of the phone number to enable/disable some + // widgets. + // Looks like we're getting a callback at creation time, so we can't + // suppose that we are enabled when the text is modified... + doEnable(mEmulatorConsole != null); + } + }); + + mVoiceButton = new Button(phoneComp, SWT.RADIO); + GridData gd = new GridData(); + gd.horizontalSpan = 2; + mVoiceButton.setText("Voice"); + mVoiceButton.setLayoutData(gd); + mVoiceButton.setEnabled(false); + mVoiceButton.setSelection(true); + mVoiceButton.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + doEnable(true); + + if (mVoiceButton.getSelection()) { + mCallButton.setText("Call"); + } else { + mCallButton.setText("Send"); + } + } + }); + + mSmsButton = new Button(phoneComp, SWT.RADIO); + mSmsButton.setText("SMS"); + gd = new GridData(); + gd.horizontalSpan = 2; + mSmsButton.setLayoutData(gd); + mSmsButton.setEnabled(false); + // Since there are only 2 radio buttons, we can put a listener on only one (they + // are both called on select and unselect event. + + mMessageLabel = new Label(phoneComp, SWT.NONE); + gd = new GridData(); + gd.verticalAlignment = SWT.TOP; + mMessageLabel.setLayoutData(gd); + mMessageLabel.setText("Message:"); + mMessageLabel.setEnabled(false); + + mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL); + mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 70; + mSmsMessage.setEnabled(false); + + // composite to put the 2 buttons horizontally + Composite g2ButtonComp = new Composite(g2, SWT.NONE); + g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + gl = new GridLayout(2, false); + gl.marginWidth = gl.marginHeight = 0; + g2ButtonComp.setLayout(gl); + + // now a button below the phone number + mCallButton = new Button(g2ButtonComp, SWT.PUSH); + mCallButton.setText("Call"); + mCallButton.setEnabled(false); + mCallButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim())); + } else { + // we need to encode the message. We need to replace the carriage return + // character by the 2 character string \n. + // Because of this the \ character needs to be escaped as well. + // ReplaceAll() expects regexp so \ char are escaped twice. + String message = mSmsMessage.getText(); + message = message.replaceAll("\\\\", //$NON-NLS-1$ + "\\\\\\\\"); //$NON-NLS-1$ + + // While the normal line delimiter is returned by Text.getLineDelimiter() + // it seems copy pasting text coming from somewhere else could have another + // delimited. For this reason, we'll replace is several steps + + // replace the dual CR-LF + message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \n + message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \r + message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(), + message)); + } + } + } + }); + + mCancelButton = new Button(g2ButtonComp, SWT.PUSH); + mCancelButton.setText("Hang Up"); + mCancelButton.setEnabled(false); + mCancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.cancelCall( + mPhoneNumber.getText().trim())); + } + } + } + }); + } + + /** + * Create Location controls. + * @param top + */ + private void createLocationControls(final Composite top) { + Label l = new Label(top, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + l.setText("Location Controls"); + + mLocationFolders = new TabFolder(top, SWT.NONE); + mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE); + TabItem item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("Manual"); + item.setControl(manualLocationComp); + + createManualLocationControl(manualLocationComp); + + mPlayImage = mImageLoader.loadImage("play.png", mParent.getDisplay()); // $NON-NLS-1$ + mPauseImage = mImageLoader.loadImage("pause.png", mParent.getDisplay()); // $NON-NLS-1$ + + Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("GPX"); + item.setControl(gpxLocationComp); + + createGpxLocationControl(gpxLocationComp); + + Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE); + kmlLocationComp.setLayout(new FillLayout()); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("KML"); + item.setControl(kmlLocationComp); + + createKmlLocationControl(kmlLocationComp); + } + + private void createManualLocationControl(Composite manualLocationComp) { + final StackLayout sl; + GridLayout gl; + Label label; + + manualLocationComp.setLayout(new GridLayout(1, false)); + mDecimalButton = new Button(manualLocationComp, SWT.RADIO); + mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDecimalButton.setText("Decimal"); + mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO); + mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSexagesimalButton.setText("Sexagesimal"); + + // composite to hold and switching between the 2 modes. + final Composite content = new Composite(manualLocationComp, SWT.NONE); + content.setLayout(sl = new StackLayout()); + + // decimal display + final Composite decimalContent = new Composite(content, SWT.NONE); + decimalContent.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mLongitudeControls = new CoordinateControls(); + mLatitudeControls = new CoordinateControls(); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createDecimalText(decimalContent); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createDecimalText(decimalContent); + + // sexagesimal content + final Composite sexagesimalContent = new Composite(content, SWT.NONE); + sexagesimalContent.setLayout(gl = new GridLayout(7, false)); + gl.marginHeight = gl.marginWidth = 0; + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); // degree character + + mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLongitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); + + mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLatitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + // set the default display to decimal + sl.topControl = decimalContent; + mDecimalButton.setSelection(true); + + mDecimalButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mDecimalButton.getSelection()) { + sl.topControl = decimalContent; + } else { + sl.topControl = sexagesimalContent; + } + content.layout(); + } + }); + + Button sendButton = new Button(manualLocationComp, SWT.PUSH); + sendButton.setText("Send"); + sendButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.sendLocation( + mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0)); + } + } + }); + + mLongitudeControls.setValue(DEFAULT_LONGITUDE); + mLatitudeControls.setValue(DEFAULT_LATITUDE); + } + + private void createGpxLocationControl(Composite gpxLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + gpxLocationComp.setLayout(new GridLayout(1, false)); + + mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH); + mGpxUploadButton.setText("Load GPX..."); + + // Table for way point + mGpxWayPointTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxWayPointTable.setHeaderVisible(true); + mGpxWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable); + gpxWayPointViewer.setContentProvider(new WayPointContentProvider()); + gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + // table for tracks. + mGpxTrackTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxTrackTable.setHeaderVisible(true); + mGpxTrackTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT, + "Some very long name", + PREFS_TRACK_COL_NAME, store); + TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT, + "9999", + PREFS_TRACK_COL_COUNT, store); + TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_FIRST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_LAST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT, + "-199.999999", + PREFS_TRACK_COL_COMMENT, store); + + final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable); + gpxTrackViewer.setContentProvider(new TrackContentProvider()); + gpxTrackViewer.setLabelProvider(new TrackLabelProvider()); + + gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + TrackPoint[] points = track.getPoints(); + processCommandResult(mEmulatorConsole.sendLocation( + points[0].getLongitude(), points[0].getLatitude(), + points[0].getElevation())); + } + + mPlayGpxButton.setEnabled(true); + mGpxBackwardButton.setEnabled(true); + mGpxForwardButton.setEnabled(true); + mGpxSpeedButton.setEnabled(true); + + return; + } + } + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + } + }); + + mGpxUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load GPX File"); + fileDialog.setFilterExtensions(new String[] { "*.gpx" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + GpxParser parser = new GpxParser(fileName); + if (parser.parse()) { + gpxWayPointViewer.setInput(parser.getWayPoints()); + gpxTrackViewer.setInput(parser.getTracks()); + } + } + } + }); + + mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE); + GridLayout gl; + mGpxPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + mPlayGpxButton.setImage(mPlayImage); + mPlayGpxButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + ISelection selection = gpxTrackViewer.getSelection(); + if (selection.isEmpty() == false && selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + playTrack(track); + } + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxBackwardButton.setSelection(false); + mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxForwardButton.setSelection(true); + mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + + } + + private void createKmlLocationControl(Composite kmlLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + kmlLocationComp.setLayout(new GridLayout(1, false)); + + mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH); + mKmlUploadButton.setText("Load KML..."); + + // Table for way point + mKmlWayPointTable = new Table(kmlLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 200; + mKmlWayPointTable.setHeaderVisible(true); + mKmlWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable); + kmlWayPointViewer.setContentProvider(new WayPointContentProvider()); + kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + mKmlUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load KML File"); + fileDialog.setFilterExtensions(new String[] { "*.kml" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + KmlParser parser = new KmlParser(fileName); + if (parser.parse()) { + kmlWayPointViewer.setInput(parser.getWayPoints()); + + mPlayKmlButton.setEnabled(true); + mKmlBackwardButton.setEnabled(true); + mKmlForwardButton.setEnabled(true); + mKmlSpeedButton.setEnabled(true); + } + } + } + }); + + kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + + + mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE); + GridLayout gl; + mKmlPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + mPlayKmlButton.setImage(mPlayImage); + mPlayKmlButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + Object input = kmlWayPointViewer.getInput(); + if (input instanceof WayPoint[]) { + playKml((WayPoint[])input); + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlBackwardButton.setSelection(false); + mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlForwardButton.setSelection(true); + mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayKmlButton.setEnabled(false); + mKmlBackwardButton.setEnabled(false); + mKmlForwardButton.setEnabled(false); + mKmlSpeedButton.setEnabled(false); + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + } + + @Override + protected void postCreation() { + // pass + } + + private synchronized void setDataMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmDataMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setVoiceMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmVoiceMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setNetworkLatency(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex)); + } + } + + private synchronized void setNetworkSpeed(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex)); + } + } + + + /** + * Callback on device selection change. + * @param device the new selected device + */ + public void handleNewDevice(Device device) { + if (mParent.isDisposed()) { + return; + } + // unlink to previous console. + synchronized (this) { + mEmulatorConsole = null; + } + + try { + // get the emulator console for this device + // First we need the device itself + if (device != null) { + GsmStatus gsm = null; + NetworkStatus netstatus = null; + + synchronized (this) { + mEmulatorConsole = EmulatorConsole.getConsole(device); + if (mEmulatorConsole != null) { + // get the gsm status + gsm = mEmulatorConsole.getGsmStatus(); + netstatus = mEmulatorConsole.getNetworkStatus(); + + if (gsm == null || netstatus == null) { + mEmulatorConsole = null; + } + } + } + + if (gsm != null && netstatus != null) { + Display d = mParent.getDisplay(); + if (d.isDisposed() == false) { + final GsmStatus f_gsm = gsm; + final NetworkStatus f_netstatus = netstatus; + + d.asyncExec(new Runnable() { + public void run() { + if (f_gsm.voice != GsmMode.UNKNOWN) { + mVoiceMode.select(getGsmComboIndex(f_gsm.voice)); + } else { + mVoiceMode.clearSelection(); + } + if (f_gsm.data != GsmMode.UNKNOWN) { + mDataMode.select(getGsmComboIndex(f_gsm.data)); + } else { + mDataMode.clearSelection(); + } + + if (f_netstatus.speed != -1) { + mNetworkSpeed.select(f_netstatus.speed); + } else { + mNetworkSpeed.clearSelection(); + } + + if (f_netstatus.latency != -1) { + mNetworkLatency.select(f_netstatus.latency); + } else { + mNetworkLatency.clearSelection(); + } + } + }); + } + } + } + } finally { + // enable/disable the ui + boolean enable = false; + synchronized (this) { + enable = mEmulatorConsole != null; + } + + enable(enable); + } + } + + /** + * Enable or disable the ui. Can be called from non ui threads. + * @param enabled + */ + private void enable(final boolean enabled) { + try { + Display d = mParent.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + doEnable(enabled); + } + } + }); + } catch (SWTException e) { + // disposed. do nothing + } + } + + private boolean isValidPhoneNumber() { + String number = mPhoneNumber.getText().trim(); + + return number.matches(RE_PHONE_NUMBER); + } + + /** + * Enable or disable the ui. Cannot be called from non ui threads. + * @param enabled + */ + protected void doEnable(boolean enabled) { + mVoiceLabel.setEnabled(enabled); + mVoiceMode.setEnabled(enabled); + + mDataLabel.setEnabled(enabled); + mDataMode.setEnabled(enabled); + + mSpeedLabel.setEnabled(enabled); + mNetworkSpeed.setEnabled(enabled); + + mLatencyLabel.setEnabled(enabled); + mNetworkLatency.setEnabled(enabled); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mPhoneNumber.isEnabled() != enabled) { + mNumberLabel.setEnabled(enabled); + mPhoneNumber.setEnabled(enabled); + } + + boolean valid = isValidPhoneNumber(); + + mVoiceButton.setEnabled(enabled && valid); + mSmsButton.setEnabled(enabled && valid); + + boolean smsValid = enabled && valid && mSmsButton.getSelection(); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mSmsMessage.isEnabled() != smsValid) { + mMessageLabel.setEnabled(smsValid); + mSmsMessage.setEnabled(smsValid); + } + if (enabled == false) { + mSmsMessage.setText(""); //$NON-NLs-1$ + } + + mCallButton.setEnabled(enabled && valid); + mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection()); + + if (enabled == false) { + mVoiceMode.clearSelection(); + mDataMode.clearSelection(); + mNetworkSpeed.clearSelection(); + mNetworkLatency.clearSelection(); + if (mPhoneNumber.getText().length() > 0) { + mPhoneNumber.setText(""); //$NON-NLS-1$ + } + } + + // location controls + mLocationFolders.setEnabled(enabled); + + mDecimalButton.setEnabled(enabled); + mSexagesimalButton.setEnabled(enabled); + mLongitudeControls.setEnabled(enabled); + mLatitudeControls.setEnabled(enabled); + + mGpxUploadButton.setEnabled(enabled); + mGpxWayPointTable.setEnabled(enabled); + mGpxTrackTable.setEnabled(enabled); + mKmlUploadButton.setEnabled(enabled); + mKmlWayPointTable.setEnabled(enabled); + } + + /** + * Returns the index of the combo item matching a specific GsmMode. + * @param mode + */ + private int getGsmComboIndex(GsmMode mode) { + for (int i = 0 ; i < GSM_MODES.length; i++) { + String[] modes = GSM_MODES[i]; + if (mode.getTag().equals(modes[1])) { + return i; + } + } + return -1; + } + + /** + * Processes the result of a command sent to the console. + * @param result the result of the command. + */ + private boolean processCommandResult(final String result) { + if (result != EmulatorConsole.RESULT_OK) { + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + MessageDialog.openError(mParent.getShell(), "Emulator Console", + result); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + + return false; + } + + return true; + } + + /** + * @param track + */ + private void playTrack(final Track track) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + TrackPoint[] trackPoints = track.getPoints(); + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + final TrackPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + TrackPoint nextPoint = trackPoints[nextIndex]; + + long delta = nextPoint.getTime() - trackPoint.getTime(); + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } + + private void playKml(final WayPoint[] trackPoints) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + WayPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + + long delta = 1000; // 1 second + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 0000000..977203b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1294 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.CategoryLabelPositions; +import org.jfree.chart.labels.CategoryToolTipGenerator; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.CategoryItemRenderer; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Base class for our information panels. + */ +public final class HeapPanel extends BaseHeapPanel { + private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + private static final int NOT_ENABLED = 1; + private static final int ENABLED = 2; + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final boolean DISPLAY_HEAP_BITMAP = false; + private static final boolean DISPLAY_HILBERT_BITMAP = false; + + private static final int PLACEHOLDER_HILBERT_SIZE = 200; + private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; + private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; + + private static final int[] ZOOMS = {100, 50, 25}; + + private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); + + static { + sByteFormatter.setMinimumFractionDigits(0); + sByteFormatter.setMaximumFractionDigits(1); + sLargeByteFormatter.setMinimumFractionDigits(3); + sLargeByteFormatter.setMaximumFractionDigits(3); + + sCountFormatter.setGroupingUsed(true); + } + + private Display mDisplay; + + private Composite mTop; // real top + private Label mUpdateStatus; + private Table mHeapSummary; + private Combo mDisplayMode; + + //private ScrolledComposite mScrolledComposite; + + private Composite mDisplayBase; // base of the displays. + private StackLayout mDisplayStack; + + private Composite mStatisticsBase; + private Table mStatisticsTable; + private JFreeChart mChart; + private ChartComposite mChartComposite; + private Button mGcButton; + private DefaultCategoryDataset mAllocCountDataSet; + + private Composite mLinearBase; + private Label mLinearHeapImage; + + private Composite mHilbertBase; + private Label mHilbertHeapImage; + private Group mLegend; + private Combo mZoom; + + /** Image used for the hilbert display. Since we recreate a new image every time, we + * keep this one around to dispose it. */ + private Image mHilbertImage; + private Image mLinearImage; + private Composite[] mLayout; + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE || + (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { + try { + mTop.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // display is disposed (app is quitting most likely), we do nothing. + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mTop.isDisposed()) + return; + + Client client = getCurrentClient(); + + Log.d("ddms", "HeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + + if (client.isHeapUpdateEnabled()) { + mGcButton.setEnabled(true); + mDisplayMode.setEnabled(true); + setUpdateStatus(ENABLED); + } else { + setUpdateStatus(NOT_ENABLED); + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + } + + fillSummaryTable(cd); + + int mode = mDisplayMode.getSelectionIndex(); + if (mode == 0) { + fillDetailedTable(client, false /* forceRedraw */); + } else { + if (DISPLAY_HEAP_BITMAP) { + renderHeapData(cd, mode - 1, false /* forceRedraw */); + } + } + } else { + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + fillSummaryTable(null); + fillDetailedTable(null, true); + setUpdateStatus(NOT_SELECTED); + } + + // sizes of things change frequently, so redo layout + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + GridLayout gl; + + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mUpdateStatus = new Label(mTop, SWT.NONE); + setUpdateStatus(NOT_SELECTED); + + Composite summarySection = new Composite(mTop, SWT.NONE); + summarySection.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mHeapSummary = createSummaryTable(summarySection); + mGcButton = new Button(summarySection, SWT.PUSH); + mGcButton.setText("Cause GC"); + mGcButton.setEnabled(false); + mGcButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client client = getCurrentClient(); + if (client != null) { + client.executeGarbageCollector(); + } + } + }); + + Composite comboSection = new Composite(mTop, SWT.NONE); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + comboSection.setLayout(gl); + + Label displayLabel = new Label(comboSection, SWT.NONE); + displayLabel.setText("Display: "); + + mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); + mDisplayMode.setEnabled(false); + mDisplayMode.add("Stats"); + if (DISPLAY_HEAP_BITMAP) { + mDisplayMode.add("Linear"); + if (DISPLAY_HILBERT_BITMAP) { + mDisplayMode.add("Hilbert"); + } + } + + // the base of the displays. + mDisplayBase = new Composite(mTop, SWT.NONE); + mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDisplayStack = new StackLayout(); + mDisplayBase.setLayout(mDisplayStack); + + // create the statistics display + mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); + //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStatisticsBase.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mDisplayStack.topControl = mStatisticsBase; + + mStatisticsTable = createDetailedTable(mStatisticsBase); + mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createChart(); + + //create the linear composite + mLinearBase = new Composite(mDisplayBase, SWT.NONE); + //mLinearBase.setLayoutData(new GridData()); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + mLinearBase.setLayout(gl); + + { + mLinearHeapImage = new Label(mLinearBase, SWT.NONE); + mLinearHeapImage.setLayoutData(new GridData()); + mLinearHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the bottom part (legend) + Composite bottomSection = new Composite(mLinearBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + bottomSection.setLayout(gl); + + createLegend(bottomSection); + } + +/* + mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL); + mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mScrolledComposite.setExpandHorizontal(true); + mScrolledComposite.setExpandVertical(true); + mScrolledComposite.setContent(mDisplayBase); +*/ + + + // create the hilbert display. + mHilbertBase = new Composite(mDisplayBase, SWT.NONE); + //mHilbertBase.setLayoutData(new GridData()); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + mHilbertBase.setLayout(gl); + + if (DISPLAY_HILBERT_BITMAP) { + mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); + mHilbertHeapImage.setLayoutData(new GridData()); + mHilbertHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the right part (legend + zoom) + Composite rightSection = new Composite(mHilbertBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + rightSection.setLayout(gl); + + Composite zoomComposite = new Composite(rightSection, SWT.NONE); + gl = new GridLayout(2, false); + zoomComposite.setLayout(gl); + + Label l = new Label(zoomComposite, SWT.NONE); + l.setText("Zoom:"); + mZoom = new Combo(zoomComposite, SWT.READ_ONLY); + for (int z : ZOOMS) { + mZoom.add(String.format("%1$d%%", z)); // $NON-NLS-1$ + } + + mZoom.select(0); + mZoom.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setLegendText(mZoom.getSelectionIndex()); + Client client = getCurrentClient(); + if (client != null) { + renderHeapData(client.getClientData(), 1, true); + mTop.pack(); + } + } + }); + + createLegend(rightSection); + } + mHilbertBase.pack(); + + mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; + mDisplayMode.select(0); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mDisplayMode.getSelectionIndex(); + Client client = getCurrentClient(); + + if (client != null) { + if (index == 0) { + fillDetailedTable(client, true /* forceRedraw */); + } else { + renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); + } + } + + mDisplayStack.topControl = mLayout[index]; + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + }); + + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + + return mTop; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mHeapSummary.setFocus(); + } + + + private Table createSummaryTable(Composite base) { + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableColumn col; + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("ID"); + col.pack(); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Heap Size"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Allocated"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Free"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.00%"); // $NON-NLS-1$ + col.pack(); + col.setText("% Used"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000,000,000"); // $NON-NLS-1$ + col.pack(); + col.setText("# Objects"); + + return tab; + } + + private Table createDetailedTable(Composite base) { + IPreferenceStore store = DdmUiPreferences.getStore(); + + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableHelper.createTableColumn(tab, "Type", SWT.LEFT, + "4-byte array (object[], int[], float[])", //$NON-NLS-1$ + PREFS_STATS_COL_TYPE, store); + + TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, + "00,000", //$NON-NLS-1$ + PREFS_STATS_COL_COUNT, store); + + TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SIZE, store); + + TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SMALLEST, store); + + TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_LARGEST, store); + + TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_MEDIAN, store); + + TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_AVERAGE, store); + + tab.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + + Client client = getCurrentClient(); + if (client != null) { + int index = mStatisticsTable.getSelectionIndex(); + TableItem item = mStatisticsTable.getItem(index); + + if (item != null) { + Map<Integer, ArrayList<HeapSegmentElement>> heapMap = + client.getClientData().getVmHeapData().getProcessedHeapMap(); + + ArrayList<HeapSegmentElement> list = heapMap.get(item.getData()); + if (list != null) { + showChart(list); + } + } + } + + } + }); + + return tab; + } + + /** + * Creates the chart below the statistics table + */ + private void createChart() { + mAllocCountDataSet = new DefaultCategoryDataset(); + mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, + PlotOrientation.VERTICAL, false, true, false); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = mStatisticsBase.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), + firstFontData, true /* ensureSameSize */); + + mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); + + Plot plot = mChart.getPlot(); + if (plot instanceof CategoryPlot) { + // get the plot + CategoryPlot categoryPlot = (CategoryPlot)plot; + + // set the domain axis to draw labels that are displayed even with many values. + CategoryAxis domainAxis = categoryPlot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); + + CategoryItemRenderer renderer = categoryPlot.getRenderer(); + renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { + public String generateToolTip(CategoryDataset dataset, int row, int column) { + // get the key for the size of the allocation + ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); + String rowKey = (String)dataset.getRowKey(row); + Number value = dataset.getValue(rowKey, columnKey); + + return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, + columnKey.getValue()); + } + }); + } + mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); // tooltips + + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + } + + private static String prettyByteCount(long bytes) { + double fracBytes = bytes; + String units = " B"; + if (fracBytes < 1024) { + return sByteFormatter.format(fracBytes) + units; + } else { + fracBytes /= 1024; + units = " KB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " MB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " GB"; + } + + return sLargeByteFormatter.format(fracBytes) + units; + } + + private static String approximateByteCount(long bytes) { + double fracBytes = bytes; + String units = ""; + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "K"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "M"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "G"; + } + + return sByteFormatter.format(fracBytes) + units; + } + + private static String addCommasToNumber(long num) { + return sCountFormatter.format(num); + } + + private static String fractionalPercent(long num, long denom) { + double val = (double)num / (double)denom; + val *= 100; + + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumFractionDigits(2); + nf.setMaximumFractionDigits(2); + return nf.format(val) + "%"; + } + + private void fillSummaryTable(ClientData cd) { + if (mHeapSummary.isDisposed()) { + return; + } + + mHeapSummary.setRedraw(false); + mHeapSummary.removeAll(); + + if (cd != null) { + synchronized (cd) { + Iterator<Integer> iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + Integer id = iter.next(); + Map<String, Long> heapInfo = cd.getVmHeapInfo(id); + if (heapInfo == null) { + continue; + } + long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES); + long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED); + long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED); + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(sizeInBytes)); + item.setText(2, prettyByteCount(bytesAllocated)); + item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated)); + item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes)); + item.setText(5, addCommasToNumber(objectsAllocated)); + } + } + } + + mHeapSummary.pack(); + mHeapSummary.setRedraw(true); + } + + private void fillDetailedTable(Client client, boolean forceRedraw) { + // first check if the client is invalid or heap updates are not enabled. + if (client == null || client.isHeapUpdateEnabled() == false) { + mStatisticsTable.removeAll(); + showChart(null); + return; + } + + ClientData cd = client.getClientData(); + + Map<Integer, ArrayList<HeapSegmentElement>> heapMap; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + heapMap = cd.getVmHeapData().getProcessedHeapMap(); + } + + // we have new data, lets display it. + + // First, get the current selection, and its key. + int index = mStatisticsTable.getSelectionIndex(); + Integer selectedKey = null; + if (index != -1) { + selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); + } + + // disable redraws and remove all from the table. + mStatisticsTable.setRedraw(false); + mStatisticsTable.removeAll(); + + if (heapMap != null) { + int selectedIndex = -1; + ArrayList<HeapSegmentElement> selectedList = null; + + // get the keys + Set<Integer> keys = heapMap.keySet(); + int iter = 0; // use a manual iter int because Set<?> doesn't have an index + // based accessor. + for (Integer key : keys) { + ArrayList<HeapSegmentElement> list = heapMap.get(key); + + // check if this is the key that is supposed to be selected + if (key.equals(selectedKey)) { + selectedIndex = iter; + selectedList = list; + } + iter++; + + TableItem item = new TableItem(mStatisticsTable, SWT.NONE); + item.setData(key); + + // get the type + item.setText(0, mMapLegend[key]); + + // set the count, smallest, largest + int count = list.size(); + item.setText(1, addCommasToNumber(count)); + + if (count > 0) { + item.setText(3, prettyByteCount(list.get(0).getLength())); + item.setText(4, prettyByteCount(list.get(count-1).getLength())); + + int median = count / 2; + HeapSegmentElement element = list.get(median); + long size = element.getLength(); + item.setText(5, prettyByteCount(size)); + + long totalSize = 0; + for (int i = 0 ; i < count; i++) { + element = list.get(i); + + size = element.getLength(); + totalSize += size; + } + + // set the average and total + item.setText(2, prettyByteCount(totalSize)); + item.setText(6, prettyByteCount(totalSize / count)); + } + } + + mStatisticsTable.setRedraw(true); + + if (selectedIndex != -1) { + mStatisticsTable.setSelection(selectedIndex); + showChart(selectedList); + } else { + showChart(null); + } + } else { + mStatisticsTable.setRedraw(true); + } + } + + private static class ByteLong implements Comparable<ByteLong> { + private long mValue; + + private ByteLong(long value) { + mValue = value; + } + + public long getValue() { + return mValue; + } + + @Override + public String toString() { + return approximateByteCount(mValue); + } + + public int compareTo(ByteLong other) { + if (mValue != other.mValue) { + return mValue < other.mValue ? -1 : 1; + } + return 0; + } + + } + + /** + * Fills the chart with the content of the list of {@link HeapSegmentElement}. + */ + private void showChart(ArrayList<HeapSegmentElement> list) { + mAllocCountDataSet.clear(); + + if (list != null) { + String rowKey = "Alloc Count"; + + long currentSize = -1; + int currentCount = 0; + for (HeapSegmentElement element : list) { + if (element.getLength() != currentSize) { + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + + currentSize = element.getLength(); + currentCount = 1; + } else { + currentCount++; + } + } + + // add the last item + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + } + } + + /* + * Add a color legend to the specified table. + */ + private void createLegend(Composite parent) { + mLegend = new Group(parent, SWT.NONE); + mLegend.setText(getLegendText(0)); + + mLegend.setLayout(new GridLayout(2, false)); + + RGB[] colors = mMapPalette.colors; + + for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { + Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); + + Label l = new Label(mLegend, SWT.NONE); + l.setImage(tmpImage); + + l = new Label(mLegend, SWT.NONE); + l.setText(mMapLegend[i]); + } + } + + private String getLegendText(int level) { + int bytes = 8 * (100 / ZOOMS[level]); + + return String.format("Key (1 pixel = %1$d bytes)", bytes); + } + + private void setLegendText(int level) { + mLegend.setText(getLegendText(level)); + + } + + /* + * Create a nice rectangle in the specified color. + */ + private Image createColorRect(Display display, RGB color) { + int width = 32; + int height = 16; + + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setBackground(new Color(display, color)); + gc.fillRectangle(0, 0, width, height); + gc.dispose(); + return img; + } + + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap updates"); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + + "NOT ENABLED for this client"); + break; + case ENABLED: + mUpdateStatus.setText("Heap updates will happen after " + + "every GC for this client"); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + + /** + * Return the closest power of two greater than or equal to value. + * + * @param value the return value will be >= value + * @return a power of two >= value. If value > 2^31, 2^31 is returned. + */ +//xxx use Integer.highestOneBit() or numberOfLeadingZeros(). + private int nextPow2(int value) { + for (int i = 31; i >= 0; --i) { + if ((value & (1<<i)) != 0) { + if (i < 31) { + return 1<<(i + 1); + } else { + return 1<<31; + } + } + } + return 0; + } + + private int zOrderData(ImageData id, byte pixData[]) { + int maxX = 0; + for (int i = 0; i < pixData.length; i++) { + /* Tread the pixData index as a z-order curve index and + * decompose into Cartesian coordinates. + */ + int x = (i & 1) | + ((i >>> 2) & 1) << 1 | + ((i >>> 4) & 1) << 2 | + ((i >>> 6) & 1) << 3 | + ((i >>> 8) & 1) << 4 | + ((i >>> 10) & 1) << 5 | + ((i >>> 12) & 1) << 6 | + ((i >>> 14) & 1) << 7 | + ((i >>> 16) & 1) << 8 | + ((i >>> 18) & 1) << 9 | + ((i >>> 20) & 1) << 10 | + ((i >>> 22) & 1) << 11 | + ((i >>> 24) & 1) << 12 | + ((i >>> 26) & 1) << 13 | + ((i >>> 28) & 1) << 14 | + ((i >>> 30) & 1) << 15; + int y = ((i >>> 1) & 1) << 0 | + ((i >>> 3) & 1) << 1 | + ((i >>> 5) & 1) << 2 | + ((i >>> 7) & 1) << 3 | + ((i >>> 9) & 1) << 4 | + ((i >>> 11) & 1) << 5 | + ((i >>> 13) & 1) << 6 | + ((i >>> 15) & 1) << 7 | + ((i >>> 17) & 1) << 8 | + ((i >>> 19) & 1) << 9 | + ((i >>> 21) & 1) << 10 | + ((i >>> 23) & 1) << 11 | + ((i >>> 25) & 1) << 12 | + ((i >>> 27) & 1) << 13 | + ((i >>> 29) & 1) << 14 | + ((i >>> 31) & 1) << 15; + try { + id.setPixel(x, y, pixData[i]); + if (x > maxX) { + maxX = x; + } + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: i " + i + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } + return maxX; + } + + private final static int HILBERT_DIR_N = 0; + private final static int HILBERT_DIR_S = 1; + private final static int HILBERT_DIR_E = 2; + private final static int HILBERT_DIR_W = 3; + + private void hilbertWalk(ImageData id, InputStream pixData, + int order, int x, int y, int dir) + throws IOException { + if (x >= id.width || y >= id.height) { + return; + } else if (order == 0) { + try { + int p = pixData.read(); + if (p >= 0) { + // flip along x=y axis; assume width == height + id.setPixel(y, x, p); + + /* Skanky; use an otherwise-unused ImageData field + * to keep track of the max x,y used. Note that x and y are inverted. + */ + if (y > id.x) { + id.x = y; + } + if (x > id.y) { + id.y = x; + } + } +//xxx just give up; don't bother walking the rest of the image + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: order " + order + + ", dir " + dir + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } else { + order--; + int delta = 1 << order; + int nextX = x + delta; + int nextY = y + delta; + + switch (dir) { + case HILBERT_DIR_E: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); + break; + case HILBERT_DIR_N: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); + break; + case HILBERT_DIR_S: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); + break; + case HILBERT_DIR_W: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); + break; + default: + throw new RuntimeException("Unexpected Hilbert direction " + + dir); + } + } + } + + private Point hilbertOrderData(ImageData id, byte pixData[]) { + + int order = 0; + for (int n = 1; n < id.width; n *= 2) { + order++; + } + /* Skanky; use an otherwise-unused ImageData field + * to keep track of maxX. + */ + Point p = new Point(0,0); + int oldIdX = id.x; + int oldIdY = id.y; + id.x = id.y = 0; + try { + hilbertWalk(id, new ByteArrayInputStream(pixData), + order, 0, 0, HILBERT_DIR_E); + p.x = id.x; + p.y = id.y; + } catch (IOException ex) { + System.err.println("Exception during hilbertWalk()"); + p.x = id.height; + p.y = id.width; + } + id.x = oldIdX; + id.y = oldIdY; + return p; + } + + private ImageData createHilbertHeapImage(byte pixData[]) { + int w, h; + + // Pick an image size that the largest of heaps will fit into. + w = (int)Math.sqrt((double)((16 * 1024 * 1024)/8)); + + // Space-filling curves require a power-of-2 width. + w = nextPow2(w); + h = w; + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, mMapPalette); + + // Copy the data into the image + //int maxX = zOrderData(id, pixData); + Point maxP = hilbertOrderData(id, pixData); + + // update the max size to make it a round number once the zoom is applied + int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; + if (factor != 1) { + int tmp = maxP.x % factor; + if (tmp != 0) { + maxP.x += factor - tmp; + } + + tmp = maxP.y % factor; + if (tmp != 0) { + maxP.y += factor - tmp; + } + } + + if (maxP.y < id.height) { + // Crop the image down to the interesting part. + id = new ImageData(id.width, maxP.y, id.depth, id.palette, + id.scanlinePad, id.data); + } + + if (maxP.x < id.width) { + // crop the image again. A bit trickier this time. + ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); + + int[] buffer = new int[maxP.x]; + for (int l = 0 ; l < id.height; l++) { + id.getPixels(0, l, maxP.x, buffer, 0); + croppedId.setPixels(0, l, maxP.x, buffer, 0); + } + + id = croppedId; + } + + // apply the zoom + if (factor != 1) { + id = id.scaledTo(id.width / factor, id.height / factor); + } + + return id; + } + + /** + * Convert the raw heap data to an image. We know we're running in + * the UI thread, so we can issue graphics commands directly. + * + * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html + * + * @param cd The client data + * @param mode The display mode. 0 = linear, 1 = hilbert. + * @param forceRedraw + */ + private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { + Image image; + + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + } + + if (pixData != null) { + ImageData id; + if (mode == 1) { + id = createHilbertHeapImage(pixData); + } else { + id = createLinearHeapImage(pixData, 200, mMapPalette); + } + + image = new Image(mDisplay, id); + } else { + // Render a placeholder image. + int width, height; + if (mode == 1) { + width = height = PLACEHOLDER_HILBERT_SIZE; + } else { + width = PLACEHOLDER_LINEAR_H_SIZE; + height = PLACEHOLDER_LINEAR_V_SIZE; + } + image = new Image(mDisplay, width, height); + GC gc = new GC(image); + gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(0, 0, width-1, height-1); + gc.dispose(); + gc = null; + } + + // set the new image + + if (mode == 1) { + if (mHilbertImage != null) { + mHilbertImage.dispose(); + } + + mHilbertImage = image; + mHilbertHeapImage.setImage(mHilbertImage); + mHilbertHeapImage.pack(true); + mHilbertBase.layout(); + mHilbertBase.pack(true); + } else { + if (mLinearImage != null) { + mLinearImage.dispose(); + } + + mLinearImage = image; + mLinearHeapImage.setImage(mLinearImage); + mLinearHeapImage.pack(true); + mLinearBase.layout(); + mLinearBase.pack(true); + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mHeapSummary); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java new file mode 100644 index 0000000..bcbf612 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +/** + * Interface defining an image loader. jar app/lib and plugin have different packaging method + * so each implementation will be different. + * The implementation should implement at least one of the methods, and preferably both if possible. + * + */ +public interface IImageLoader { + + /** + * Load an image from the resource from a filename + * @param filename + * @param display + */ + public Image loadImage(String filename, Display display); + + /** + * Load an ImageDescriptor from the resource from a filename + * @param filename + * @param display + */ + public ImageDescriptor loadDescriptor(String filename, Display display); + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 0000000..bf425d9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.swt.dnd.Clipboard; + +/** + * An object listening to focus change in Table objects.<br> + * For application not relying on a RCP to provide menu changes based on focus, + * this class allows to get monitor the focus change of several Table widget + * and update the menu action accordingly. + */ +public interface ITableFocusListener { + + public interface IFocusedTableActivator { + public void copy(Clipboard clipboard); + + public void selectAll(); + } + + public void focusGained(IFocusedTableActivator activator); + + public void focusLost(IFocusedTableActivator activator); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java new file mode 100644 index 0000000..d65978b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class ImageHelper { + + /** + * Loads an image from a resource. This method used a class to locate the + * resources, and then load the filename from /images inside the resources.<br> + * Extra parameters allows for creation of a replacement image of the + * loading failed. + * + * @param loader the image loader used. + * @param display the Display object + * @param fileName the file name + * @param width optional width to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param height optional height to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param phColor optional color to create replacement Image. If null, Blue + * color will be used. + * @return a new Image or null if the loading failed and the optional + * replacement size was -1 + */ + public static Image loadImage(IImageLoader loader, Display display, + String fileName, int width, int height, Color phColor) { + + Image img = null; + if (loader != null) { + img = loader.loadImage(fileName, display); + } + + if (img == null) { + Log.w("ddms", "Couldn't load " + fileName); + // if we had the extra parameter to create replacement image then we + // create and return it. + if (width != -1 && height != -1) { + return createPlaceHolderArt(display, width, height, + phColor != null ? phColor : display + .getSystemColor(SWT.COLOR_BLUE)); + } + + // otherwise, just return null + return null; + } + + return img; + } + + /** + * Create place-holder art with the specified color. + */ + public static Image createPlaceHolderArt(Display display, int width, + int height, Color color) { + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setForeground(color); + gc.drawLine(0, 0, width, height); + gc.drawLine(0, height - 1, width, -1); + gc.dispose(); + return img; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java new file mode 100644 index 0000000..76f2285 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.InputStream; + +/** + * Image loader for an normal standalone app. + */ +public class ImageLoader implements IImageLoader { + + /** class used as reference to get the reources */ + private Class<?> mClass; + + /** + * Creates a loader for a specific class. The class allows java to figure + * out which .jar file to search for the image. + * + * @param theClass + */ + public ImageLoader(Class<?> theClass) { + mClass = theClass; + } + + public ImageDescriptor loadDescriptor(String filename, Display display) { + // we don't support ImageDescriptor + return null; + } + + public Image loadImage(String filename, Display display) { + + String tmp = "/images/" + filename; + InputStream imageStream = mClass.getResourceAsStream(tmp); + + if (imageStream != null) { + Image img = new Image(display, imageStream); + if (img == null) + throw new NullPointerException("couldn't load " + tmp); + return img; + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 0000000..72cbb4a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +/** + * Display client info in a two-column format. + */ +public class InfoPanel extends TablePanel { + private Table mTable; + private TableColumn mCol2; + + private static final String mLabels[] = { + "DDM-aware?", + "App description:", + "VM version:", + "Process ID:", + }; + private static final int ENT_DDM_AWARE = 0; + private static final int ENT_APP_DESCR = 1; + private static final int ENT_VM_VERSION = 2; + private static final int ENT_PROCESS_ID = 3; + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mTable.setHeaderVisible(false); + mTable.setLinesVisible(false); + + TableColumn col1 = new TableColumn(mTable, SWT.RIGHT); + col1.setText("name"); + mCol2 = new TableColumn(mTable, SWT.LEFT); + mCol2.setText("PlaceHolderContentForWidth"); + + TableItem item; + for (int i = 0; i < mLabels.length; i++) { + item = new TableItem(mTable, SWT.NONE); + item.setText(0, mLabels[i]); + item.setText(1, "-"); + } + + col1.pack(); + mCol2.pack(); + + return mTable; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTable.setFocus(); + } + + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) { + if (mTable.isDisposed()) + return; + + mTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + if (mTable.isDisposed()) + return; + + Client client = getCurrentClient(); + + if (client == null) { + for (int i = 0; i < mLabels.length; i++) { + TableItem item = mTable.getItem(i); + item.setText(1, "-"); + } + } else { + TableItem item; + String clientDescription, vmIdentifier, isDdmAware, + pid; + + ClientData cd = client.getClientData(); + synchronized (cd) { + clientDescription = (cd.getClientDescription() != null) ? + cd.getClientDescription() : "?"; + vmIdentifier = (cd.getVmIdentifier() != null) ? + cd.getVmIdentifier() : "?"; + isDdmAware = cd.isDdmAware() ? + "yes" : "no"; + pid = (cd.getPid() != 0) ? + String.valueOf(cd.getPid()) : "?"; + } + + item = mTable.getItem(ENT_APP_DESCR); + item.setText(1, clientDescription); + item = mTable.getItem(ENT_VM_VERSION); + item.setText(1, vmIdentifier); + item = mTable.getItem(ENT_DDM_AWARE); + item.setText(1, isDdmAware); + item = mTable.getItem(ENT_PROCESS_ID); + item.setText(1, pid); + } + + mCol2.pack(); + + //Log.i("ddms", "InfoPanel: changed " + client); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mTable); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java new file mode 100644 index 0000000..46461bf --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java @@ -0,0 +1,1633 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; +import com.android.ddmuilib.annotation.WorkerThread; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Panel with native heap information. + */ +public final class NativeHeapPanel extends BaseHeapPanel { + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final int ALLOC_DISPLAY_ALL = 0; + private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1; + private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2; + + private Display mDisplay; + + private Composite mBase; + + private Label mUpdateStatus; + + /** combo giving choice of what to display: all, pre-zygote, post-zygote */ + private Combo mAllocDisplayCombo; + + private Button mFullUpdateButton; + + // see CreateControl() + //private Button mDiffUpdateButton; + + private Combo mDisplayModeCombo; + + /** stack composite for mode (1-2) & 3 */ + private Composite mTopStackComposite; + + private StackLayout mTopStackLayout; + + /** stack composite for mode 1 & 2 */ + private Composite mAllocationStackComposite; + + private StackLayout mAllocationStackLayout; + + /** top level container for mode 1 & 2 */ + private Composite mTableModeControl; + + /** top level object for the allocation mode */ + private Control mAllocationModeTop; + + /** top level for the library mode */ + private Control mLibraryModeTopControl; + + /** composite for page UI and total memory display */ + private Composite mPageUIComposite; + + private Label mTotalMemoryLabel; + + private Label mPageLabel; + + private Button mPageNextButton; + + private Button mPagePreviousButton; + + private Table mAllocationTable; + + private Table mLibraryTable; + + private Table mLibraryAllocationTable; + + private Table mDetailTable; + + private Label mImage; + + private int mAllocDisplayMode = ALLOC_DISPLAY_ALL; + + /** + * pointer to current stackcall thread computation in order to quit it if + * required (new update requested) + */ + private StackCallThread mStackCallThread; + + /** Current Library Allocation table fill thread. killed if selection changes */ + private FillTableThread mFillTableThread; + + /** + * current client data. Used to access the malloc info when switching pages + * or selecting allocation to show stack call + */ + private ClientData mClientData; + + /** + * client data from a previous display. used when asking for an "update & diff" + */ + private ClientData mBackUpClientData; + + /** list of NativeAllocationInfo objects filled with the list from ClientData */ + private final ArrayList<NativeAllocationInfo> mAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of the {@link NativeAllocationInfo} being displayed based on the selection + * of {@link #mAllocDisplayCombo}. + */ + private final ArrayList<NativeAllocationInfo> mDisplayedAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */ + private final ArrayList<NativeAllocationInfo> mBackUpAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** back up of the total memory, used when doing an "update & diff" */ + private int mBackUpTotalMemory; + + private int mCurrentPage = 0; + + private int mPageCount = 0; + + /** + * list of allocation per Library. This is created from the list of + * NativeAllocationInfo objects that is stored in the ClientData object. Since we + * don't keep this list around, it is recomputed everytime the client + * changes. + */ + private final ArrayList<LibraryAllocations> mLibraryAllocations = + new ArrayList<LibraryAllocations>(); + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + + private static final int NOT_ENABLED = 1; + + private static final int ENABLED = 2; + + private static final int DISPLAY_PER_PAGE = 20; + + private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$ + private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$ + private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$ + private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$ + private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$ + + /** static formatter object to format all numbers as #,### */ + private static DecimalFormat sFormatter; + static { + sFormatter = (DecimalFormat)NumberFormat.getInstance(); + if (sFormatter != null) + sFormatter = new DecimalFormat("#,###"); + else + sFormatter.applyPattern("#,###"); + } + + + /** + * caching mechanism to avoid recomputing the backtrace for a particular + * address several times. + */ + private HashMap<Long, NativeStackCallInfo> mSourceCache = + new HashMap<Long,NativeStackCallInfo>(); + private long mTotalSize; + private Button mSaveButton; + private Button mSymbolsButton; + + /** + * thread class to convert the address call into method, file and line + * number in the background. + */ + private class StackCallThread extends BackgroundThread { + private ClientData mClientData; + + public StackCallThread(ClientData cd) { + mClientData = cd; + } + + public ClientData getClientData() { + return mClientData; + } + + @Override + public void run() { + // loop through all the NativeAllocationInfo and init them + Iterator<NativeAllocationInfo> iter = mAllocations.iterator(); + int total = mAllocations.size(); + int count = 0; + while (iter.hasNext()) { + + if (isQuitting()) + return; + + NativeAllocationInfo info = iter.next(); + if (info.isStackCallResolved() == false) { + final Long[] list = info.getStackCallAddresses(); + final int size = list.length; + + ArrayList<NativeStackCallInfo> resolvedStackCall = + new ArrayList<NativeStackCallInfo>(); + + for (int i = 0; i < size; i++) { + long addr = list[i]; + + // first check if the addr has already been converted. + NativeStackCallInfo source = mSourceCache.get(addr); + + // if not we convert it + if (source == null) { + source = sourceForAddr(addr); + mSourceCache.put(addr, source); + } + + resolvedStackCall.add(source); + } + + info.setResolvedStackCall(resolvedStackCall); + } + // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless + // we reach total, since we also do it after the loop + // (only an issue in case we have a perfect number of page) + count++; + if ((count % DISPLAY_PER_PAGE) == 0 && count != total) { + if (updateNHAllocationStackCalls(mClientData, count) == false) { + // looks like the app is quitting, so we just + // stopped the thread + return; + } + } + } + + updateNHAllocationStackCalls(mClientData, count); + } + + private NativeStackCallInfo sourceForAddr(long addr) { + NativeLibraryMapInfo library = getLibraryFor(addr); + + if (library != null) { + + Addr2Line process = Addr2Line.getProcess(library.getLibraryName()); + if (process != null) { + // remove the base of the library address + long value = addr - library.getStartAddress(); + NativeStackCallInfo info = process.getAddress(value); + if (info != null) { + return info; + } + } + } + + return new NativeStackCallInfo(library != null ? library.getLibraryName() : null, + Long.toHexString(addr), ""); + } + + private NativeLibraryMapInfo getLibraryFor(long addr) { + Iterator<NativeLibraryMapInfo> it = mClientData.getNativeLibraryMapInfo(); + + while (it.hasNext()) { + NativeLibraryMapInfo info = it.next(); + + if (info.isWithinLibrary(addr)) { + return info; + } + } + + Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); + return null; + } + + /** + * update the Native Heap panel with the amount of allocation for which the + * stack call has been computed. This is called from a non UI thread, but + * will be executed in the UI thread. + * + * @param count the amount of allocation + * @return false if the display was disposed and the update couldn't happen + */ + private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateAllocationStackCalls(clientData, count); + } + }); + return true; + } + return false; + } + } + + private class FillTableThread extends BackgroundThread { + private LibraryAllocations mLibAlloc; + + private int mMax; + + public FillTableThread(LibraryAllocations liballoc, int m) { + mLibAlloc = liballoc; + mMax = m; + } + + @Override + public void run() { + for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) { + updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10); + } + } + + /** + * updates the library allocation table in the Native Heap panel. This is + * called from a non UI thread, but will be executed in the UI thread. + * + * @param liballoc the current library allocation object being displayed + * @param start start index of items that need to be displayed + * @param end end index of the items that need to be displayed + */ + private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc, + final int start, final int end) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateLibraryAllocationTable(libAlloc, start, end); + } + }); + } + + } + } + + /** class to aggregate allocations per library */ + public static class LibraryAllocations { + private String mLibrary; + + private final ArrayList<NativeAllocationInfo> mLibAllocations = + new ArrayList<NativeAllocationInfo>(); + + private int mSize; + + private int mCount; + + /** construct the aggregate object for a library */ + public LibraryAllocations(final String lib) { + mLibrary = lib; + } + + /** get the library name */ + public String getLibrary() { + return mLibrary; + } + + /** add a NativeAllocationInfo object to this aggregate object */ + public void addAllocation(NativeAllocationInfo info) { + mLibAllocations.add(info); + } + + /** get an iterator on the NativeAllocationInfo objects */ + public Iterator<NativeAllocationInfo> getAllocations() { + return mLibAllocations.iterator(); + } + + /** get a NativeAllocationInfo object by index */ + public NativeAllocationInfo getAllocation(int index) { + return mLibAllocations.get(index); + } + + /** returns the NativeAllocationInfo object count */ + public int getAllocationSize() { + return mLibAllocations.size(); + } + + /** returns the total allocation size */ + public int getSize() { + return mSize; + } + + /** returns the number of allocations */ + public int getCount() { + return mCount; + } + + /** + * compute the allocation count and size for allocation objects added + * through <code>addAllocation()</code>, and sort the objects by + * total allocation size. + */ + public void computeAllocationSizeAndCount() { + mSize = 0; + mCount = 0; + for (NativeAllocationInfo info : mLibAllocations) { + mCount += info.getAllocationCount(); + mSize += info.getAllocationCount() * info.getSize(); + } + Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() { + public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) { + return o2.getAllocationCount() * o2.getSize() - + o1.getAllocationCount() * o1.getSize(); + } + }); + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + + mDisplay = parent.getDisplay(); + + mBase = new Composite(parent, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.horizontalSpacing = 0; + gl.verticalSpacing = 0; + mBase.setLayout(gl); + mBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // composite for <update btn> <status> + Composite tmp = new Composite(mBase, SWT.NONE); + tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + tmp.setLayout(gl = new GridLayout(2, false)); + gl.marginWidth = gl.marginHeight = 0; + + mFullUpdateButton = new Button(tmp, SWT.NONE); + mFullUpdateButton.setText("Full Update"); + mFullUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mBackUpClientData = null; + mDisplayModeCombo.setEnabled(false); + mSaveButton.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + + mUpdateStatus = new Label(tmp, SWT.NONE); + mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // top layout for the combos and oter controls on the right. + Composite top_layout = new Composite(mBase, SWT.NONE); + top_layout.setLayout(gl = new GridLayout(4, false)); + gl.marginWidth = gl.marginHeight = 0; + + new Label(top_layout, SWT.NONE).setText("Show:"); + + mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mAllocDisplayCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mAllocDisplayCombo.add("All Allocations"); + mAllocDisplayCombo.add("Pre-Zygote Allocations"); + mAllocDisplayCombo.add("Zygote Child Allocations (Z)"); + mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onAllocDisplayChange(); + } + }); + mAllocDisplayCombo.select(0); + + // separator + Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL); + GridData gd; + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + gd.verticalSpan = 2; + + mSaveButton = new Button(top_layout, SWT.PUSH); + mSaveButton.setText("Save..."); + mSaveButton.setEnabled(false); + mSaveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE); + + fileDialog.setText("Save Allocations"); + fileDialog.setFileName("allocations.txt"); + + String fileName = fileDialog.open(); + if (fileName != null) { + saveAllocations(fileName); + } + } + }); + + /* + * TODO: either fix the diff mechanism or remove it altogether. + mDiffUpdateButton = new Button(top_layout, SWT.NONE); + mDiffUpdateButton.setText("Update && Diff"); + mDiffUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // since this is an update and diff, we need to store the + // current list + // of mallocs + mBackUpAllocations.clear(); + mBackUpAllocations.addAll(mAllocations); + mBackUpClientData = mClientData; + mBackUpTotalMemory = mClientData.getTotalNativeMemory(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + */ + + Label l = new Label(top_layout, SWT.NONE); + l.setText("Display:"); + + mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mDisplayModeCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" }); + mDisplayModeCombo.select(0); + mDisplayModeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + switchDisplayMode(); + } + }); + mDisplayModeCombo.setEnabled(false); + + mSymbolsButton = new Button(top_layout, SWT.PUSH); + mSymbolsButton.setText("Load Symbols"); + mSymbolsButton.setEnabled(false); + + + // create a composite that will contains the actual content composites, + // in stack mode layout. + // This top level composite contains 2 other composites. + // * one for both Allocations and Libraries mode + // * one for flat mode (which is gone for now) + + mTopStackComposite = new Composite(mBase, SWT.NONE); + mTopStackComposite.setLayout(mTopStackLayout = new StackLayout()); + mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create 1st and 2nd modes + createTableDisplay(mTopStackComposite); + + mTopStackLayout.topControl = mTableModeControl; + mTopStackComposite.layout(); + + setUpdateStatus(NOT_SELECTED); + + // Work in progress + // TODO add image display of native heap. + //mImage = new Label(mBase, SWT.NONE); + + mBase.pack(); + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + // TODO + } + + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) { + if (mBase.isDisposed()) + return; + + mBase.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mBase.isDisposed()) + return; + + Client client = getCurrentClient(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + + Log.d("ddms", "NativeHeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + mClientData = cd; + + // if (cd.getShowHeapUpdates()) + setUpdateStatus(ENABLED); + // else + // setUpdateStatus(NOT_ENABLED); + + initAllocationDisplay(); + + //renderBitmap(cd); + } else { + mClientData = null; + setUpdateStatus(NOT_SELECTED); + } + + mBase.pack(); + } + + /** + * Update the UI with the newly compute stack calls, unless the UI switched + * to a different client. + * + * @param cd the ClientData for which the stack call are being computed. + * @param count the current count of allocations for which the stack calls + * have been computed. + */ + @WorkerThread + public void updateAllocationStackCalls(ClientData cd, int count) { + // we have to check that the panel still shows the same clientdata than + // the thread is computing for. + if (cd == mClientData) { + + int total = mAllocations.size(); + + if (count == total) { + // we're done: do something + mDisplayModeCombo.setEnabled(true); + mSaveButton.setEnabled(true); + + mStackCallThread = null; + } else { + // work in progress, update the progress bar. +// mUiThread.setStatusLine("Computing stack call: " + count +// + "/" + total); + } + + // FIXME: attempt to only update when needed. + // Because the number of pages is not related to mAllocations.size() anymore + // due to pre-zygote/post-zygote display, update all the time. + // At some point we should remove the pages anyway, since it's getting computed + // really fast now. +// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count +// || (count == total && mCurrentPage == mPageCount - 1)) { + try { + // get the current selection of the allocation + int index = mAllocationTable.getSelectionIndex(); + NativeAllocationInfo info = null; + + if (index != -1) { + info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData(); + } + + // empty the table + emptyTables(); + + // fill it again + fillAllocationTable(); + + // reselect + mAllocationTable.setSelection(index); + + // display detail table if needed + if (info != null) { + fillDetailTable(info); + } + } catch (SWTException e) { + if (mAllocationTable.isDisposed()) { + // looks like the table is disposed. Let's ignore it. + } else { + throw e; + } + } + + } else { + // old client still running. doesn't really matter. + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mLibraryTable); + addTableToFocusListener(mLibraryAllocationTable); + addTableToFocusListener(mDetailTable); + } + + protected void onAllocDisplayChange() { + mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex(); + + // create the new list + updateAllocDisplayList(); + + updateTotalMemoryDisplay(); + + // reset the ui. + mCurrentPage = 0; + updatePageUI(); + switchDisplayMode(); + } + + private void updateAllocDisplayList() { + mTotalSize = 0; + mDisplayedAllocations.clear(); + for (NativeAllocationInfo info : mAllocations) { + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL || + (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) { + mDisplayedAllocations.add(info); + mTotalSize += info.getSize() * info.getAllocationCount(); + } else { + // skip this item + continue; + } + } + + int count = mDisplayedAllocations.size(); + + mPageCount = count / DISPLAY_PER_PAGE; + + // need to add a page for the rest of the div + if ((count % DISPLAY_PER_PAGE) > 0) { + mPageCount++; + } + } + + private void updateTotalMemoryDisplay() { + switch (mAllocDisplayMode) { + case ALLOC_DISPLAY_ALL: + mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_PRE_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_POST_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + } + } + + + private void switchDisplayMode() { + switch (mDisplayModeCombo.getSelectionIndex()) { + case 0: {// allocations + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mAllocationModeTop; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillAllocationTable(); + } + break; + case 1: {// libraries + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mLibraryModeTopControl; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillLibraryTable(); + } + break; + } + } + + private void initAllocationDisplay() { + mAllocations.clear(); + mAllocations.addAll(mClientData.getNativeAllocationList()); + + updateAllocDisplayList(); + + // if we have a previous clientdata and it matches the current one. we + // do a diff between the new list and the old one. + if (mBackUpClientData != null && mBackUpClientData == mClientData) { + + ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>(); + + // we go through the list of NativeAllocationInfo in the new list and check if + // there's one with the same exact data (size, allocation, count and + // stackcall addresses) in the old list. + // if we don't find any, we add it to the "add" list + for (NativeAllocationInfo mi : mAllocations) { + boolean found = false; + for (NativeAllocationInfo old_mi : mBackUpAllocations) { + if (mi.equals(old_mi)) { + found = true; + break; + } + } + if (found == false) { + add.add(mi); + } + } + + // put the result in mAllocations + mAllocations.clear(); + mAllocations.addAll(add); + + // display the difference in memory usage. This is computed + // calculating the memory usage of the objects in mAllocations. + int count = 0; + for (NativeAllocationInfo allocInfo : mAllocations) { + count += allocInfo.getSize() * allocInfo.getAllocationCount(); + } + + mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes", + sFormatter.format(count))); + } + else { + // display the full memory usage + updateTotalMemoryDisplay(); + //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0); + } + mTotalMemoryLabel.pack(); + + // update the page ui + mDisplayModeCombo.select(0); + + mLibraryAllocations.clear(); + + // reset to first page + mCurrentPage = 0; + + // update the label + updatePageUI(); + + // now fill the allocation Table with the current page + switchDisplayMode(); + + // start the thread to compute the stack calls + if (mAllocations.size() > 0) { + mStackCallThread = new StackCallThread(mClientData); + mStackCallThread.start(); + } + } + + private void updatePageUI() { + + // set the label and pack to update the layout, otherwise + // the label will be cut off if the new size is bigger + if (mPageCount == 0) { + mPageLabel.setText("0 of 0 allocations."); + } else { + StringBuffer buffer = new StringBuffer(); + // get our starting index + int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1; + // end index, taking into account the last page can be half full + int count = mDisplayedAllocations.size(); + int end = Math.min(start + DISPLAY_PER_PAGE - 1, count); + buffer.append(sFormatter.format(start)); + buffer.append(" - "); + buffer.append(sFormatter.format(end)); + buffer.append(" of "); + buffer.append(sFormatter.format(count)); + buffer.append(" allocations."); + mPageLabel.setText(buffer.toString()); + } + + // handle the button enabled state. + mPagePreviousButton.setEnabled(mCurrentPage > 0); + // reminder: mCurrentPage starts at 0. + mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1); + + mPageLabel.pack(); + mPageUIComposite.pack(); + + } + + private void fillAllocationTable() { + // get the count + int count = mDisplayedAllocations.size(); + + // get our starting index + int start = mCurrentPage * DISPLAY_PER_PAGE; + + // loop for DISPLAY_PER_PAGE or till we reach count + int end = start + DISPLAY_PER_PAGE; + + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = mDisplayedAllocations.get(i); + + TableItem item = null; + + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, (info.isZygoteChild() ? "Z " : "") + + sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else { + // skip this item + continue; + } + + item.setData(info); + + NativeStackCallInfo bti = info.getRelevantStackCallInfo(); + if (bti != null) { + String lib = bti.getLibraryName(); + String method = bti.getMethodName(); + String source = bti.getSourceFile(); + if (lib != null) + item.setText(3, lib); + if (method != null) + item.setText(4, method); + if (source != null) + item.setText(5, source); + } + } + } + + private void fillLibraryTable() { + // fill the library table + sortAllocationsPerLibrary(); + + for (LibraryAllocations liballoc : mLibraryAllocations) { + if (liballoc != null) { + TableItem item = new TableItem(mLibraryTable, SWT.NONE); + String lib = liballoc.getLibrary(); + item.setText(0, lib != null ? lib : ""); + item.setText(1, sFormatter.format(liballoc.getSize())); + item.setText(2, sFormatter.format(liballoc.getCount())); + } + } + } + + private void fillLibraryAllocationTable() { + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations liballoc = mLibraryAllocations.get(index); + // start a thread that will fill table 10 at a time to keep the ui + // responsive, but first we kill the previous one if there was one + if (mFillTableThread != null) { + mFillTableThread.quit(); + } + mFillTableThread = new FillTableThread(liballoc, + liballoc.getAllocationSize()); + mFillTableThread.start(); + } + } + + public void updateLibraryAllocationTable(LibraryAllocations liballoc, + int start, int end) { + try { + if (mLibraryTable.isDisposed() == false) { + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations newliballoc = mLibraryAllocations.get( + index); + if (newliballoc == liballoc) { + int count = liballoc.getAllocationSize(); + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = liballoc.getAllocation(i); + + TableItem item = new TableItem( + mLibraryAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format( + info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + + NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + item.setText(3, stackCallInfo.getMethodName()); + } + } + } else { + // we should quit the thread + if (mFillTableThread != null) { + mFillTableThread.quit(); + mFillTableThread = null; + } + } + } + } + } catch (SWTException e) { + Log.e("ddms", "error when updating the library allocation table"); + } + } + + private void fillDetailTable(final NativeAllocationInfo mi) { + mDetailTable.removeAll(); + mDetailTable.setRedraw(false); + + try { + // populate the detail Table with the back trace + Long[] addresses = mi.getStackCallAddresses(); + NativeStackCallInfo[] resolvedStackCall = mi.getResolvedStackCall(); + + if (resolvedStackCall == null) { + return; + } + + for (int i = 0 ; i < resolvedStackCall.length ; i++) { + if (addresses[i] == null || addresses[i].longValue() == 0) { + continue; + } + + long addr = addresses[i].longValue(); + NativeStackCallInfo source = resolvedStackCall[i]; + + TableItem item = new TableItem(mDetailTable, SWT.NONE); + item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$ + + String libraryName = source.getLibraryName(); + String methodName = source.getMethodName(); + String sourceFile = source.getSourceFile(); + int lineNumber = source.getLineNumber(); + + if (libraryName != null) + item.setText(1, libraryName); + if (methodName != null) + item.setText(2, methodName); + if (sourceFile != null) + item.setText(3, sourceFile); + if (lineNumber != -1) + item.setText(4, Integer.toString(lineNumber)); + } + } finally { + mDetailTable.setRedraw(true); + } + } + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap info"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case ENABLED: + mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data"); + mAllocDisplayCombo.setEnabled(true); + mFullUpdateButton.setEnabled(true); + //mDiffUpdateButton.setEnabled(true); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + /** + * Create the Table display. This includes a "detail" Table in the bottom + * half and 2 modes in the top half: allocation Table and + * library+allocations Tables. + * + * @param base the top parent to create the display into + */ + private void createTableDisplay(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // top level composite for mode 1 & 2 + mTableModeControl = new Composite(base, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + mTableModeControl.setLayout(gl); + mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE); + mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTotalMemoryLabel.setText("Total Memory: 0 Bytes"); + + // the top half of these modes is dynamic + + final Composite sash_composite = new Composite(mTableModeControl, + SWT.NONE); + sash_composite.setLayout(new FormLayout()); + sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create the stacked composite + mAllocationStackComposite = new Composite(sash_composite, SWT.NONE); + mAllocationStackLayout = new StackLayout(); + mAllocationStackComposite.setLayout(mAllocationStackLayout); + mAllocationStackComposite.setLayoutData(new GridData( + GridData.FILL_BOTH)); + + // create the top half for mode 1 + createAllocationTopHalf(mAllocationStackComposite); + + // create the top half for mode 2 + createLibraryTopHalf(mAllocationStackComposite); + + final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL); + + // bottom half of these modes is the same: detail table + createDetailTable(sash_composite); + + // init value for stack + mAllocationStackLayout.topControl = mAllocationModeTop; + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(mTotalMemoryLabel, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mAllocationStackComposite.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) { + sashData.top = new FormAttachment(0, + prefs.getInt(PREFS_ALLOCATION_SASH)); + } else { + sashData.top = new FormAttachment(50, 0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mDetailTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = sash_composite.getClientArea(); + int bottom = panelRect.height - sashRect.height - minPanelWidth; + e.y = Math.max(Math.min(e.y, bottom), minPanelWidth); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFS_ALLOCATION_SASH, e.y); + sash_composite.layout(); + } + } + }); + } + + private void createDetailTable(Composite base) { + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDetailTable.setHeaderVisible(true); + mDetailTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT, + "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT, + "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$ + } + + private void createAllocationTopHalf(Composite b) { + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + Composite base = new Composite(b, SWT.NONE); + mAllocationModeTop = base; + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.verticalSpacing = 0; + base.setLayout(gl); + base.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // horizontal layout for memory total and pages UI + mPageUIComposite = new Composite(base, SWT.NONE); + mPageUIComposite.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_BEGINNING)); + gl = new GridLayout(3, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.horizontalSpacing = 0; + mPageUIComposite.setLayout(gl); + + // Page UI + mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE); + mPagePreviousButton.setText("<"); + mPagePreviousButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage--; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageNextButton = new Button(mPageUIComposite, SWT.NONE); + mPageNextButton.setText(">"); + mPageNextButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage++; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageLabel = new Label(mPageUIComposite, SWT.NONE); + mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + updatePageUI(); + + mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT, + "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT, + "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT, + "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$ + + mAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mAllocationTable.getSelectionIndex(); + TableItem item = mAllocationTable.getItem(index); + if (item != null && item.getData() instanceof NativeAllocationInfo) { + fillDetailTable((NativeAllocationInfo)item.getData()); + } + } + }); + } + + private void createLibraryTopHalf(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // create a composite that'll contain 2 tables horizontally + final Composite top = new Composite(base, SWT.NONE); + mLibraryModeTopControl = top; + top.setLayout(new FormLayout()); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // first table: library + mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryTable.setHeaderVisible(true); + mLibraryTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT, + "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT, + "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$ + + mLibraryTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + fillLibraryAllocationTable(); + } + }); + + final Sash sash = new Sash(top, SWT.VERTICAL); + + // 2nd table: allocation per library + mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryAllocationTable.setHeaderVisible(true); + mLibraryAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryAllocationTable, "Total", + SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Count", + SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Size", + SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Method", + SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$ + + mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the index of the selection in the library table + int index1 = mLibraryTable.getSelectionIndex(); + // get the index in the library allocation table + int index2 = mLibraryAllocationTable.getSelectionIndex(); + // get the MallocInfo object + LibraryAllocations liballoc = mLibraryAllocations.get(index1); + NativeAllocationInfo info = liballoc.getAllocation(index2); + fillDetailTable(info); + } + }); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + mLibraryTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) { + sashData.left = new FormAttachment(0, + prefs.getInt(PREFS_LIBRARY_SASH)); + } else { + sashData.left = new FormAttachment(50, 0); + } + sashData.bottom = new FormAttachment(100, 0); + sashData.top = new FormAttachment(0, 0); // 50% across + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + mLibraryAllocationTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = top.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFS_LIBRARY_SASH, e.y); + top.layout(); + } + } + }); + } + + private void emptyTables() { + mAllocationTable.removeAll(); + mLibraryTable.removeAll(); + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + } + + private void sortAllocationsPerLibrary() { + if (mClientData != null) { + mLibraryAllocations.clear(); + + // create a hash map of LibraryAllocations to access aggregate + // objects already created + HashMap<String, LibraryAllocations> libcache = + new HashMap<String, LibraryAllocations>(); + + // get the allocation count + int count = mDisplayedAllocations.size(); + for (int i = 0; i < count; i++) { + NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i); + + NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + String libraryName = stackCallInfo.getLibraryName(); + LibraryAllocations liballoc = libcache.get(libraryName); + if (liballoc == null) { + // didn't find a library allocation object already + // created so we create one + liballoc = new LibraryAllocations(libraryName); + // add it to the cache + libcache.put(libraryName, liballoc); + // add it to the list + mLibraryAllocations.add(liballoc); + } + // add the MallocInfo object to it. + liballoc.addAllocation(allocInfo); + } + } + // now that the list is created, we need to compute the size and + // sort it by size. This will also sort the MallocInfo objects + // inside each LibraryAllocation objects. + for (LibraryAllocations liballoc : mLibraryAllocations) { + liballoc.computeAllocationSizeAndCount(); + } + + // now we sort it + Collections.sort(mLibraryAllocations, + new Comparator<LibraryAllocations>() { + public int compare(LibraryAllocations o1, + LibraryAllocations o2) { + return o2.getSize() - o1.getSize(); + } + }); + } + } + + private void renderBitmap(ClientData cd) { + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + + ImageData id = createLinearHeapImage(pixData, 200, mMapPalette); + Image image = new Image(mBase.getDisplay(), id); + mImage.setImage(image); + mImage.pack(true); + } + } + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + private void saveAllocations(String fileName) { + try { + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); + + for (NativeAllocationInfo alloc : mAllocations) { + out.println(alloc.toString()); + } + out.close(); + } catch (IOException e) { + Log.e("Native", e); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java new file mode 100644 index 0000000..d910cc7 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + + +/** + * Base class for our information panels. + */ +public abstract class Panel { + + public final Control createPanel(Composite parent) { + Control panelControl = createControl(parent); + + postCreation(); + + return panelControl; + } + + protected abstract void postCreation(); + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + protected abstract Control createControl(Composite parent); + + /** + * Sets the focus to the proper control inside the panel. + */ + public abstract void setFocus(); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java new file mode 100644 index 0000000..533372e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Edit an integer field, validating it as a port number. + */ +public class PortFieldEditor extends IntegerFieldEditor { + + public boolean mRecursiveCheck = false; + + public PortFieldEditor(String name, String label, Composite parent) { + super(name, label, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + /* + * Get the current value of the field, as an integer. + */ + public int getCurrentValue() { + int val; + try { + val = Integer.parseInt(getStringValue()); + } + catch (NumberFormatException nfe) { + val = -1; + } + return val; + } + + /* + * Check the validity of the field. + */ + @Override + protected boolean checkState() { + if (super.checkState() == false) { + return false; + } + //Log.i("ddms", "check state " + getStringValue()); + boolean err = false; + int val = getCurrentValue(); + if (val < 1024 || val > 32767) { + setErrorMessage("Port must be between 1024 and 32767"); + err = true; + } else { + setErrorMessage(null); + err = false; + } + showErrorMessage(); + return !err; + } + + protected void updateCheckState(PortFieldEditor pfe) { + pfe.refreshValidState(); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 0000000..dad54dc --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; + + +/** + * Gather a screen shot from the device and save it to a file. + */ +public class ScreenShotDialog extends Dialog { + + private Label mBusyLabel; + private Label mImageLabel; + private Button mSave; + private Device mDevice; + + + /** + * Create with default style. + */ + public ScreenShotDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create with app-defined style. + */ + public ScreenShotDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + * @param device The {@link Device} from which to get the screenshot. + */ + public void open(Device device) { + mDevice = device; + + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Device Screen Capture"); + + createContents(shell); + shell.pack(); + shell.open(); + + updateDeviceImage(shell); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + } + + /* + * Create the screen capture dialog contents. + */ + private void createContents(final Shell shell) { + GridData data; + + shell.setLayout(new GridLayout(3, true)); + + // title/"capturing" label + mBusyLabel = new Label(shell, SWT.NONE); + mBusyLabel.setText("Preparing..."); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = 3; + mBusyLabel.setLayoutData(data); + + // space for the image + mImageLabel = new Label(shell, SWT.BORDER); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.horizontalSpan = 3; + mImageLabel.setLayoutData(data); + Display display = shell.getDisplay(); + mImageLabel.setImage(ImageHelper.createPlaceHolderArt(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE))); + + // "refresh" button + Button refresh = new Button(shell, SWT.PUSH); + refresh.setText("Refresh"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + refresh.setLayoutData(data); + refresh.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateDeviceImage(shell); + } + }); + + // "save" button + mSave = new Button(shell, SWT.PUSH); + mSave.setText("Save"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mSave.setLayoutData(data); + mSave.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + saveImage(shell); + } + }); + + // "done" button + Button done = new Button(shell, SWT.PUSH); + done.setText("Done"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + done.setLayoutData(data); + done.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + shell.setDefaultButton(done); + } + + /* + * Capture a new image from the device. + */ + private void updateDeviceImage(Shell shell) { + mBusyLabel.setText("Capturing..."); // no effect + + shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT)); + + Image image = getDeviceImage(); + if (image == null) { + Display display = shell.getDisplay(); + image = ImageHelper.createPlaceHolderArt(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); + mSave.setEnabled(false); + mBusyLabel.setText("Screen not available"); + } else { + mSave.setEnabled(true); + mBusyLabel.setText("Captured image:"); + } + + mImageLabel.setImage(image); + mImageLabel.pack(); + shell.pack(); + + // there's no way to restore old cursor; assume it's ARROW + shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); + } + + /* + * Grab an image from an ADB-connected device. + */ + private Image getDeviceImage() { + RawImage rawImage; + + try { + rawImage = mDevice.getScreenshot(); + } + catch (IOException ioe) { + Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage()); + return null; + } + + // device/adb not available? + if (rawImage == null) + return null; + + // convert raw data to an Image + assert rawImage.bpp == 16; + PaletteData palette = new PaletteData(0xf800, 0x07e0, 0x001f); + ImageData imageData = new ImageData(rawImage.width, rawImage.height, + rawImage.bpp, palette, 1, rawImage.data); + + return new Image(getParent().getDisplay(), imageData); + } + + /* + * Prompt the user to save the image to disk. + */ + private void saveImage(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + String fileName; + + dlg.setText("Save image..."); + dlg.setFileName("device.png"); + dlg.setFilterPath(DdmUiPreferences.getStore().getString("lastImageSaveDir")); + dlg.setFilterNames(new String[] { + "PNG Files (*.png)" + }); + dlg.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + + fileName = dlg.open(); + if (fileName != null) { + DdmUiPreferences.getStore().setValue("lastImageSaveDir", dlg.getFilterPath()); + + Log.i("ddms", "Saving image to " + fileName); + ImageData imageData = mImageLabel.getImage().getImageData(); + + try { + WritePng.savePng(fileName, imageData); + } + catch (IOException ioe) { + Log.w("ddms", "Unable to save " + fileName + ": " + ioe); + } + + if (false) { + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { imageData }; + // PNG writing not available until 3.3? See bug at: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=24697 + // GIF writing only works for 8 bits + // JPEG uses lossy compression + // BMP has screwed-up colors + loader.save(fileName, SWT.IMAGE_JPEG); + } + } + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 0000000..4b5fe56 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; + +/** + * A Panel that requires {@link Device}/{@link Client} selection notifications. + */ +public abstract class SelectionDependentPanel extends Panel { + private Device mCurrentDevice = null; + private Client mCurrentClient = null; + + /** + * Returns the current {@link Device}. + * @return the current device or null if none are selected. + */ + protected final Device getCurrentDevice() { + return mCurrentDevice; + } + + /** + * Returns the current {@link Client}. + * @return the current client or null if none are selected. + */ + protected final Client getCurrentClient() { + return mCurrentClient; + } + + /** + * Sent when a new device is selected. + * @param selectedDevice the selected device. + */ + public final void deviceSelected(Device selectedDevice) { + if (selectedDevice != mCurrentDevice) { + mCurrentDevice = selectedDevice; + deviceSelected(); + } + } + + /** + * Sent when a new client is selected. + * @param selectedClient the selected client. + */ + public final void clientSelected(Client selectedClient) { + if (selectedClient != mCurrentClient) { + mCurrentClient = selectedClient; + clientSelected(); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + public abstract void deviceSelected(); + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + public abstract void clientSelected(); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 0000000..3358962 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IStackTraceInfo; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; + +/** + * Stack Trace Panel. + * <p/>This is not a panel in the regular sense. Instead this is just an object around the creation + * and management of a Stack Trace display. + * <p/>UI creation is done through + * {@link #createPanel(Composite, String, String, String, String, String, IPreferenceStore)}. + * + */ +public final class StackTracePanel { + + private static ISourceRevealer sSourceRevealer; + + private Table mStackTraceTable; + private TableViewer mStackTraceViewer; + + private Client mCurrentClient; + + + /** + * Content Provider to display the stack trace of a thread. + * Expected input is a {@link IStackTraceInfo} object. + */ + private static class StackTraceContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof IStackTraceInfo) { + // getElement cannot return null, so we return an empty array + // if there's no stack trace + StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace(); + if (trace != null) { + return trace; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be + * of type {@link StackTraceElement}. + */ + private static class StackTraceLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)element; + switch (columnIndex) { + case 0: + return traceElement.getClassName(); + case 1: + return traceElement.getMethodName(); + case 2: + return traceElement.getFileName(); + case 3: + return Integer.toString(traceElement.getLineNumber()); + case 4: + return Boolean.toString(traceElement.isNativeMethod()); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide a method that is able to reveal a method + * in a source editor + */ + public interface ISourceRevealer { + /** + * Sent to reveal a particular line in a source editor + * @param applicationName the name of the application running the source. + * @param className the fully qualified class name + * @param line the line to reveal + */ + public void reveal(String applicationName, String className, int line); + } + + + /** + * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor. + * @param revealer + */ + public static void setSourceRevealer(ISourceRevealer revealer) { + sSourceRevealer = revealer; + } + + /** + * Creates the controls for the StrackTrace display. + * <p/>This method will set the parent {@link Composite} to use a {@link GridLayout} with + * 2 columns. + * @param parent the parent composite. + * @param prefs_stack_col_class + * @param prefs_stack_col_method + * @param prefs_stack_col_file + * @param prefs_stack_col_line + * @param prefs_stack_col_native + * @param store + */ + public Table createPanel(Composite parent, String prefs_stack_col_class, + String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line, + String prefs_stack_col_native, IPreferenceStore store) { + + mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mStackTraceTable.setHeaderVisible(true); + mStackTraceTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mStackTraceTable, + "Class", + SWT.LEFT, + "SomeLongClassName", //$NON-NLS-1$ + prefs_stack_col_class, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Method", + SWT.LEFT, + "someLongMethod", //$NON-NLS-1$ + prefs_stack_col_method, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "File", + SWT.LEFT, + "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$ + prefs_stack_col_file, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Line", + SWT.RIGHT, + "99999", //$NON-NLS-1$ + prefs_stack_col_line, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Native", + SWT.LEFT, + "Native", //$NON-NLS-1$ + prefs_stack_col_native, store); + + mStackTraceViewer = new TableViewer(mStackTraceTable); + mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); + mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); + + mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + if (sSourceRevealer != null && mCurrentClient != null) { + // get the selected stack trace element + ISelection selection = mStackTraceViewer.getSelection(); + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)object; + + if (traceElement.isNativeMethod() == false) { + sSourceRevealer.reveal( + mCurrentClient.getClientData().getClientDescription(), + traceElement.getClassName(), + traceElement.getLineNumber()); + } + } + } + } + } + }); + + return mStackTraceTable; + } + + /** + * Sets the input for the {@link TableViewer}. + * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of + * {@link StackTraceElement} + */ + public void setViewerInput(IStackTraceInfo input) { + mStackTraceViewer.setInput(input); + mStackTraceViewer.refresh(); + } + + /** + * Sets the current client running the stack trace. + * @param currentClient the {@link Client}. + */ + public void setCurrentClient(Client currentClient) { + mCurrentClient = currentClient; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 0000000..8ef237c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.experimental.chart.swt.ChartComposite; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Displays system information graphs obtained from a bugreport file or device. + */ +public class SysinfoPanel extends TablePanel implements IShellOutputReceiver { + + // UI components + private Label mLabel; + private Button mFetchButton; + private Combo mDisplayMode; + + private DefaultPieDataset mDataset; + + // The bugreport file to process + private File mDataFile; + + // To get output from adb commands + private FileOutputStream mTempStream; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + + private static final int MODE_CPU = 0; + private static final int MODE_ALARM = 1; + private static final int MODE_WAKELOCK = 2; + private static final int MODE_MEMINFO = 3; + private static final int MODE_SYNC = 4; + + // argument to dumpsys; section in the bugreport holding the data + private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm", + "batteryinfo", "MEMORY INFO", "content"}; + + private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo", + "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank", + "dumpsys content"}; + + private static final String CAPTIONS[] = {"CPU load", "Alarms", + "Wakelocks", "Memory usage", "Sync"}; + + /** + * Generates the dataset to display. + * + * @param file The bugreport file to process. + */ + public void generateDataset(File file) { + mDataset.clear(); + mLabel.setText(""); + if (file == null) { + return; + } + try { + BufferedReader br = getBugreportReader(file); + if (mMode == MODE_CPU) { + readCpuDataset(br); + } else if (mMode == MODE_ALARM) { + readAlarmDataset(br); + } else if (mMode == MODE_WAKELOCK) { + readWakelockDataset(br); + } else if (mMode == MODE_MEMINFO) { + readMeminfoDataset(br); + } else if (mMode == MODE_SYNC) { + readSyncDataset(br); + } + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed with + * {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + if (getCurrentDevice() != null) { + mFetchButton.setEnabled(true); + loadFromDevice(); + } else { + mFetchButton.setEnabled(false); + } + } + + /** + * Sent when a new client is selected. The new client can be accessed with + * {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mDisplayMode.setFocus(); + } + + /** + * Fetches a new bugreport from the device and updates the display. + * Fetching is asynchronous. See also addOutput, flush, and isCancelled. + */ + private void loadFromDevice() { + try { + initShellOutputBuffer(); + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + mTempStream.write("------ MEMORY INFO ------\n".getBytes()); + } + getCurrentDevice().executeShellCommand( + DUMP_COMMAND[mMode], this); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Initializes temporary output file for executeShellCommand(). + * + * @throws IOException on file error + */ + void initShellOutputBuffer() throws IOException { + mDataFile = File.createTempFile("ddmsfile", ".txt"); + mDataFile.deleteOnExit(); + mTempStream = new FileOutputStream(mDataFile); + } + + /** + * Adds output to the temp file. IShellOutputReceiver method. Called by + * executeShellCommand(). + */ + public void addOutput(byte[] data, int offset, int length) { + try { + mTempStream.write(data, offset, length); + } + catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Processes output from shell command. IShellOutputReceiver method. The + * output is passed to generateDataset(). Called by executeShellCommand() on + * completion. + */ + public void flush() { + if (mTempStream != null) { + try { + mTempStream.close(); + generateDataset(mDataFile); + mTempStream = null; + mDataFile = null; + } catch (IOException e) { + Log.e("DDMS", e); + } + } + } + + /** + * IShellOutputReceiver method. + * + * @return false - don't cancel + */ + public boolean isCancelled() { + return false; + } + + /** + * Create our controls for the UI panel. + */ + @Override + protected Control createControl(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Composite buttons = new Composite(top, SWT.NONE); + buttons.setLayout(new RowLayout()); + + mDisplayMode = new Combo(buttons, SWT.PUSH); + for (String mode : CAPTIONS) { + mDisplayMode.add(mode); + } + mDisplayMode.select(mMode); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mMode = mDisplayMode.getSelectionIndex(); + if (mDataFile != null) { + generateDataset(mDataFile); + } else if (getCurrentDevice() != null) { + loadFromDevice(); + } + } + }); + + final Button loadButton = new Button(buttons, SWT.PUSH); + loadButton.setText("Load from File"); + loadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(loadButton.getShell(), + SWT.OPEN); + fileDialog.setText("Load bugreport"); + String filename = fileDialog.open(); + if (filename != null) { + mDataFile = new File(filename); + generateDataset(mDataFile); + } + } + }); + + mFetchButton = new Button(buttons, SWT.PUSH); + mFetchButton.setText("Update from Device"); + mFetchButton.setEnabled(false); + mFetchButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + loadFromDevice(); + } + }); + + mLabel = new Label(top, SWT.NONE); + mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDataset = new DefaultPieDataset(); + JFreeChart chart = ChartFactory.createPieChart("", mDataset, false + /* legend */, true/* tooltips */, false /* urls */); + + ChartComposite chartComposite = new ChartComposite(top, + SWT.BORDER, chart, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, + // max draw width. We don't want it to zoom, so we put a big number + 3000, + // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); + chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + return top; + } + + public void clientChanged(final Client client, int changeMask) { + // Don't care + } + + /** + * Helper to open a bugreport and skip to the specified section. + * + * @param file File to open + * @return Reader to bugreport file + * @throws java.io.IOException on file error + */ + private BufferedReader getBugreportReader(File file) throws + IOException { + BufferedReader br = new BufferedReader(new FileReader(file)); + // Skip over the unwanted bugreport sections + while (true) { + String line = br.readLine(); + if (line == null) { + Log.d("DDMS", "Service not found " + line); + break; + } + if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) && + line.indexOf(BUGREPORT_SECTION[mMode]) > 0) { + break; + } + } + return br; + } + + /** + * Parse the time string generated by BatteryStats. + * A typical new-format string is "11d 13h 45m 39s 999ms". + * A typical old-format string is "12.3 sec". + * @return time in ms + */ + private static long parseTimeMs(String s) { + long total = 0; + // Matches a single component e.g. "12.3 sec" or "45ms" + Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); + Matcher m = p.matcher(s); + while (m.find()) { + String label = m.group(2); + if ("sec".equals(label)) { + // Backwards compatibility with old time format + total += (long) (Double.parseDouble(m.group(1)) * 1000); + continue; + } + long value = Integer.parseInt(m.group(1)); + if ("d".equals(label)) { + total += value * 24 * 60 * 60 * 1000; + } else if ("h".equals(label)) { + total += value * 60 * 60 * 1000; + } else if ("m".equals(label)) { + total += value * 60 * 1000; + } else if ("s".equals(label)) { + total += value * 1000; + } else if ("ms".equals(label)) { + total += value; + } + } + return total; + } + /** + * Processes wakelock information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readWakelockDataset(BufferedReader br) throws IOException { + Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); + Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); + double total = 0; + boolean inCurrent = false; + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Current Battery Usage Statistics")) { + inCurrent = true; + } else if (inCurrent) { + Matcher m = lockPattern.matcher(line); + if (m.find()) { + double value = parseTimeMs(m.group(2)) / 1000.; + mDataset.setValue(m.group(1), value); + total -= value; + } else { + m = totalPattern.matcher(line); + if (m.find()) { + total += parseTimeMs(m.group(1)) / 1000.; + } + } + } + } + if (total > 0) { + mDataset.setValue("Unlocked", total); + } + } + + /** + * Processes alarm information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readAlarmDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + long count = Long.parseLong(m.group(1)); + String name = m.group(2); + mDataset.setValue(name, count); + } + } + } + + /** + * Processes cpu load information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readCpuDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Load:")) { + mLabel.setText(line); + continue; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + String name = m.group(1); + long both = Long.parseLong(m.group(2)); + long user = Long.parseLong(m.group(3)); + long kernel = Long.parseLong(m.group(4)); + if ("TOTAL".equals(name)) { + if (both < 100) { + mDataset.setValue("Idle", (100 - both)); + } + } else { + // Try to make graphs more useful even with rounding; + // log often has 0% user + 0% kernel = 1% total + // We arbitrarily give extra to kernel + if (user > 0) { + mDataset.setValue(name + " (user)", user); + } + if (kernel > 0) { + mDataset.setValue(name + " (kernel)" , both - user); + } + if (user == 0 && kernel == 0 && both > 0) { + mDataset.setValue(name, both); + } + } + } + } + } + + /** + * Processes meminfo information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readMeminfoDataset(BufferedReader br) throws IOException { + Pattern valuePattern = Pattern.compile("(\\d+) kB"); + long total = 0; + long other = 0; + mLabel.setText("PSS in kB"); + + // Scan meminfo + while (true) { + String line = br.readLine(); + if (line == null) { + // End of file + break; + } + Matcher m = valuePattern.matcher(line); + if (m.find()) { + long kb = Long.parseLong(m.group(1)); + if (line.startsWith("MemTotal")) { + total = kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } else if (line.startsWith("Slab")) { + mDataset.setValue("Slab", kb); + total -= kb; + } else if (line.startsWith("PageTables")) { + mDataset.setValue("PageTables", kb); + total -= kb; + } else if (line.startsWith("Buffers") && kb > 0) { + mDataset.setValue("Buffers", kb); + total -= kb; + } else if (line.startsWith("Inactive")) { + mDataset.setValue("Inactive", kb); + total -= kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } + } else { + break; + } + } + // Scan procrank + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) { + // procrank header + continue; + } + if (line.indexOf("----") >= 0) { + //end of procrank section + break; + } + // Extract pss field from procrank output + long pss = Long.parseLong(line.substring(23, 31).trim()); + String cmdline = line.substring(43).trim().replace("/system/bin/", ""); + // Arbitrary minimum size to display + if (pss > 2000) { + mDataset.setValue(cmdline, pss); + } else { + other += pss; + } + total -= pss; + } + mDataset.setValue("Other", other); + mDataset.setValue("Unknown", total); + } + + /** + * Processes sync information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readSyncDataset(BufferedReader br) throws IOException { + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith(" |") && line.length() > 70) { + String authority = line.substring(3, 18).trim(); + String duration = line.substring(61, 70).trim(); + // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) + String durParts[] = duration.split(":"); + if (durParts.length == 2) { + long dur = Long.parseLong(durParts[0]) * 60 + Long + .parseLong(durParts[1]); + mDataset.setValue(authority, dur); + } else if (duration.length() == 3) { + long dur = Long.parseLong(durParts[0]) * 3600 + + Long.parseLong(durParts[1]) * 60 + Long + .parseLong(durParts[2]); + mDataset.setValue(authority, dur); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java new file mode 100644 index 0000000..f8d457e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +/** + * Utility class to help using Table objects. + * + */ +public final class TableHelper { + /** + * Create a TableColumn with the specified parameters. If a + * <code>PreferenceStore</code> object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + * @return The TableColumn object that was created + */ + public static TableColumn createTableColumn(Table parent, String header, + int style, String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TableColumn col = new TableColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TableColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + + return col; + } + + /** + * Create a TreeColumn with the specified parameters. If a + * <code>PreferenceStore</code> object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java new file mode 100644 index 0000000..b037193 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; + +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; + +/** + * Base class for panel containing Table that need to support copy-paste-selectAll + */ +public abstract class TablePanel extends ClientDisplayPanel { + private ITableFocusListener mGlobalListener; + + /** + * Sets a TableFocusListener which will be notified when one of the tables + * gets or loses focus. + * + * @param listener + */ + public final void setTableFocusListener(ITableFocusListener listener) { + // record the global listener, to make sure table created after + // this call will still be setup. + mGlobalListener = listener; + + setTableFocusListener(); + } + + /** + * Sets up the Table of object of the panel to work with the global listener.<br> + * Default implementation does nothing. + */ + protected void setTableFocusListener() { + + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + * @param colStart + * @param colEnd + */ + protected final void addTableToFocusListener(final Table table, + final int colStart, final int colEnd) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + public void copy(Clipboard clipboard) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + for (int c = colStart ; c <= colEnd ; c++) { + sb.append(item.getText(c)); + sb.append('\t'); + } + sb.append('\n'); + } + + // now add that to the clipboard if the string has content + String data = sb.toString(); + if (data != null || data.length() > 0) { + clipboard.setContents( + new Object[] { data }, + new Transfer[] { TextTransfer.getInstance() }); + } + } + + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus.<br> + * When the copy method is invoked, all columns are put in the clipboard, separated + * by tabs + * + * @param table the Table object. + */ + protected final void addTableToFocusListener(final Table table) { + addTableToFocusListener(table, 0, table.getColumnCount()-1); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 0000000..a034063 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ThreadInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Base class for our information panels. + */ +public class ThreadPanel extends TablePanel { + + private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ + + private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$ + + private Display mDisplay; + private Composite mBase; + private Label mNotEnabled; + private Label mNotSelected; + + private Composite mThreadBase; + private Table mThreadTable; + private TableViewer mThreadViewer; + + private Composite mStackTraceBase; + private Button mRefreshStackTraceButton; + private Label mStackTraceTimeLabel; + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + + /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ + private boolean mMustStopRecurringThreadUpdate = false; + /** Flag to tell the recurring thread update to stop running */ + private boolean mRecurringThreadUpdateRunning = false; + + private Object mLock = new Object(); + + private static final String[] THREAD_STATUS = { + "zombie", "running", "timed-wait", "monitor", + "wait", "init", "start", "native", "vmwait" + }; + + /** + * Content Provider to display the threads of a client. + * Expected input is a {@link Client} object. + */ + private static class ThreadContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + return ((Client)inputElement).getClientData().getThreads(); + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be + * of type {@link ThreadInfo}. + */ + private static class ThreadLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof ThreadInfo) { + ThreadInfo thread = (ThreadInfo)element; + switch (columnIndex) { + case 0: + return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ + String.valueOf(thread.getThreadId()); + case 1: + return String.valueOf(thread.getTid()); + case 2: + if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) + return THREAD_STATUS[thread.getStatus()]; + return "unknown"; + case 3: + return String.valueOf(thread.getUtime()); + case 4: + return String.valueOf(thread.getStime()); + case 5: + return thread.getThreadName(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + mBase = new Composite(parent, SWT.NONE); + mBase.setLayout(new StackLayout()); + + // UI for thread not enabled + mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotEnabled.setText("Thread updates not enabled for selected client\n" + + "(use toolbar button to enable)"); + + // UI for not client selected + mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotSelected.setText("no client is selected"); + + // base composite for selected client with enabled thread update. + mThreadBase = new Composite(mBase, SWT.NONE); + mThreadBase.setLayout(new FormLayout()); + + // table above the sash + mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); + mThreadTable.setHeaderVisible(true); + mThreadTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mThreadTable, + "ID", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_THREAD_COL_ID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Tid", + SWT.RIGHT, + "88888", //$NON-NLS-1$ + PREFS_THREAD_COL_TID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Status", + SWT.LEFT, + "timed-wait", //$NON-NLS-1$ + PREFS_THREAD_COL_STATUS, store); + + TableHelper.createTableColumn( + mThreadTable, + "utime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_UTIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "stime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_STIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "Name", + SWT.LEFT, + "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ + PREFS_THREAD_COL_NAME, store); + + mThreadViewer = new TableViewer(mThreadTable); + mThreadViewer.setContentProvider(new ThreadContentProvider()); + mThreadViewer.setLabelProvider(new ThreadLabelProvider()); + + mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + updateThreadStackTrace(selectedThread); + } + }); + mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + if (selectedThread != null) { + Client client = (Client)mThreadViewer.getInput(); + + if (client != null) { + client.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + // the separating sash + final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTraceBase = new Composite(mThreadBase, SWT.NONE); + mStackTraceBase.setLayout(new GridLayout(2, false)); + + mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); + mRefreshStackTraceButton.setText("Refresh"); + mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ThreadInfo selectedThread = getThreadSelection(null); + if (selectedThread != null) { + Client currentClient = getCurrentClient(); + if (currentClient != null) { + currentClient.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); + mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + GridData gd; + mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.horizontalSpan = 2; + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mThreadTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_THREAD_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceBase.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mThreadBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_THREAD_SASH, e.y); + mThreadBase.layout(); + } + } + }); + + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mThreadTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || + (changeMask & Client.CHANGE_THREAD_DATA) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + updateThreadStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mThreadTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + + if (client != null) { + if (!client.isThreadUpdateEnabled()) { + ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; + mThreadViewer.setInput(null); + + // if we are currently updating the thread, stop doing it. + mMustStopRecurringThreadUpdate = true; + } else { + ((StackLayout)mBase.getLayout()).topControl = mThreadBase; + mThreadViewer.setInput(client); + + synchronized (mLock) { + // if we're not updating we start the process + if (mRecurringThreadUpdateRunning == false) { + startRecurringThreadUpdate(); + } else if (mMustStopRecurringThreadUpdate) { + // else if there's a runnable that's still going to get called, lets + // simply cancel the stop, and keep going + mMustStopRecurringThreadUpdate = false; + } + } + } + } else { + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + mThreadViewer.setInput(null); + } + + mBase.layout(); + } + + /** + * Updates the stack call of the currently selected thread. + * <p/> + * This <b>must</b> be called from the UI thread. + */ + private void updateThreadStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + ThreadInfo selectedThread = getThreadSelection(null); + + if (selectedThread != null) { + updateThreadStackTrace(selectedThread); + } else { + updateThreadStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied + * of current data. + * @param thread + */ + private void updateThreadStackTrace(ThreadInfo thread) { + mStackTracePanel.setViewerInput(thread); + + if (thread != null) { + mRefreshStackTraceButton.setEnabled(true); + long stackcallTime = thread.getStackCallTime(); + if (stackcallTime != 0) { + String label = new Date(stackcallTime).toString(); + mStackTraceTimeLabel.setText(label); + } else { + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } else { + mRefreshStackTraceButton.setEnabled(true); + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mThreadTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Initiate recurring events. We use a shorter "initialWait" so we do the + * first execution sooner. We don't do it immediately because we want to + * give the clients a chance to get set up. + */ + private void startRecurringThreadUpdate() { + mRecurringThreadUpdateRunning = true; + int initialWait = 1000; + + mDisplay.timerExec(initialWait, new Runnable() { + public void run() { + synchronized (mLock) { + // lets check we still want updates. + if (mMustStopRecurringThreadUpdate == false) { + Client client = getCurrentClient(); + if (client != null) { + client.requestThreadUpdate(); + + mDisplay.timerExec( + DdmUiPreferences.getThreadRefreshInterval() * 1000, this); + } else { + // we don't have a Client, which means the runnable is not + // going to be called through the timer. We reset the running flag. + mRecurringThreadUpdateRunning = false; + } + } else { + // else actually stops (don't call the timerExec) and reset the flags. + mRecurringThreadUpdateRunning = false; + mMustStopRecurringThreadUpdate = false; + } + } + } + }); + } + + /** + * Returns the current thread selection or <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection + * is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + private ThreadInfo getThreadSelection(ISelection selection) { + if (selection == null) { + selection = mThreadViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof ThreadInfo) { + return (ThreadInfo)object; + } + } + + return null; + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java new file mode 100644 index 0000000..f65dafe --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.Log; + +import org.eclipse.swt.graphics.ImageData; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +/** + * Compensate for SWT issues by writing our own PNGs from ImageData. + */ +public class WritePng { + private WritePng() {} + + private static final byte[] PNG_MAGIC = + new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 }; + + public static void savePng(String fileName, ImageData imageData) + throws IOException { + + try { + FileOutputStream out = new FileOutputStream(fileName); + + Log.d("ddms", "Saving to PNG, width=" + imageData.width + + ", height=" + imageData.height + + ", depth=" + imageData.depth + + ", bpl=" + imageData.bytesPerLine); + + savePng(out, imageData); + + // need to do that on, or the file is empty on windows! + out.flush(); + out.close(); + } catch (Exception e) { + Log.e("writepng", e); + } + } + + /* + * Supply functionality missing from our version of SWT. + */ + private static void savePng(OutputStream out, ImageData imageData) + throws IOException { + + int width = imageData.width; + int height = imageData.height; + byte[] out24; + + Log.i("ddms-png", "Convert to 24bit from " + imageData.depth); + + if (imageData.depth == 24 || imageData.depth == 32) { + out24 = convertTo24ForPng(imageData.data, width, height, + imageData.depth, imageData.bytesPerLine); + } else if (imageData.depth == 16) { + out24 = convert16to24(imageData); + } else { + return; + } + + // Create the compressed form. I'm taking the low road here and + // just creating a large buffer, which should always be enough to + // hold the compressed output. + byte[] compPixels = new byte[out24.length + 16384]; + Deflater compressor = new Deflater(); + compressor.setLevel(Deflater.BEST_COMPRESSION); + compressor.setInput(out24); + compressor.finish(); + int compLen; + do { // must do this in a loop to satisfy java.util.Zip + compLen = compressor.deflate(compPixels); + assert compLen != 0 || !compressor.needsInput(); + } while (compLen == 0); + Log.d("ddms", "Compressed image data from " + out24.length + + " to " + compLen); + + // Write the PNG magic + out.write(PNG_MAGIC); + + ByteBuffer buf; + CRC32 crc; + + // Write the IHDR chunk (13 bytes) + byte[] header = new byte[8 + 13 + 4]; + buf = ByteBuffer.wrap(header); + buf.order(ByteOrder.BIG_ENDIAN); + + putChunkHeader(buf, 13, "IHDR"); + buf.putInt(width); + buf.putInt(height); + buf.put((byte) 8); // 8pp + buf.put((byte) 2); // direct color used + buf.put((byte) 0); // compression method == deflate + buf.put((byte) 0); // filter method (none) + buf.put((byte) 0); // interlace method (none) + + crc = new CRC32(); + crc.update(header, 4, 4+13); + buf.putInt((int)crc.getValue()); + + out.write(header); + + // Write the IDAT chunk + byte[] datHdr = new byte[8 + 0 + 4]; + buf = ByteBuffer.wrap(datHdr); + buf.order(ByteOrder.BIG_ENDIAN); + + putChunkHeader(buf, compLen, "IDAT"); + crc = new CRC32(); + crc.update(datHdr, 4, 4+0); + crc.update(compPixels, 0, compLen); + buf.putInt((int) crc.getValue()); + + out.write(datHdr, 0, 8); + out.write(compPixels, 0, compLen); + out.write(datHdr, 8, 4); + + // Write the IEND chunk (0 bytes) + byte[] trailer = new byte[8 + 0 + 4]; + + buf = ByteBuffer.wrap(trailer); + buf.order(ByteOrder.BIG_ENDIAN); + putChunkHeader(buf, 0, "IEND"); + + crc = new CRC32(); + crc.update(trailer, 4, 4+0); + buf.putInt((int)crc.getValue()); + + out.write(trailer); + } + + /* + * Output a chunk header. + */ + private static void putChunkHeader(ByteBuffer buf, int length, + String typeStr) { + + int type = 0; + + if (typeStr.length() != 4) + throw new RuntimeException(); + + for (int i = 0; i < 4; i++) { + type <<= 8; + type |= (byte) typeStr.charAt(i); + } + + buf.putInt(length); + buf.putInt(type); + } + + /* + * Convert raw pixels to 24-bit RGB with a "filter" byte at the start + * of each row. + */ + private static byte[] convertTo24ForPng(byte[] in, int width, int height, + int depth, int stride) { + + assert depth == 24 || depth == 32; + assert stride == width * (depth/8); + + // 24 bit pixels plus one byte per line for "filter" + byte[] out24 = new byte[width * height * 3 + height]; + int y; + + int inOff = 0; + int outOff = 0; + for (y = 0; y < height; y++) { + out24[outOff++] = 0; // filter flag + + if (depth == 24) { + System.arraycopy(in, inOff, out24, outOff, width * 3); + outOff += width * 3; + } else if (depth == 32) { + int tmpOff = inOff; + for (int x = 0; x < width; x++) { + tmpOff++; // ignore alpha + out24[outOff++] = in[tmpOff++]; + out24[outOff++] = in[tmpOff++]; + out24[outOff++] = in[tmpOff++]; + } + } + + inOff += stride; + } + + assert outOff == out24.length; + + return out24; + } + + private static byte[] convert16to24(ImageData imageData) { + int width = imageData.width; + int height = imageData.height; + + int redShift = imageData.palette.redShift; + int greenShift = imageData.palette.greenShift; + int blueShift = imageData.palette.blueShift; + + int redMask = imageData.palette.redMask; + int greenMask = imageData.palette.greenMask; + int blueMask = imageData.palette.blueMask; + + // 24 bit pixels plus one byte per line for "filter" + byte[] out24 = new byte[width * height * 3 + height]; + int outOff = 0; + + + int[] line = new int[width]; + for (int y = 0; y < height; y++) { + imageData.getPixels(0, y, width, line, 0); + + out24[outOff++] = 0; // filter flag + for (int x = 0; x < width; x++) { + int pixelValue = line[x]; + out24[outOff++] = byteChannelValue(pixelValue, redMask, redShift); + out24[outOff++] = byteChannelValue(pixelValue, greenMask, greenShift); + out24[outOff++] = byteChannelValue(pixelValue, blueMask, blueShift); + } + } + + return out24; + } + + private static byte byteChannelValue(int value, int mask, int shift) { + int bValue = value & mask; + if (shift < 0) { + bValue = bValue >>> -shift; + } else { + bValue = bValue << shift; + } + + return (byte)bValue; + + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 0000000..856b874 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.actions; + +/** + * Common interface for basic action handling. This allows the common ui + * components to access ToolItem or Action the same way. + */ +public interface ICommonAction { + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + */ + public void setEnabled(boolean enabled); + + /** + * Sets the checked status of this action. + * @param checked the new checked status + */ + public void setChecked(boolean checked); + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered. + */ + public void setRunnable(Runnable runnable); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 0000000..bc1598f --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.actions; + +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +/** + * Wrapper around {@link ToolItem} to implement {@link ICommonAction} + */ +public class ToolItemAction implements ICommonAction { + public ToolItem item; + + public ToolItemAction(ToolBar parent, int style) { + item = new ToolItem(parent, style); + } + + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + * @see ICommonAction#setChecked(boolean) + */ + public void setChecked(boolean checked) { + item.setSelection(checked); + } + + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + * @see ICommonAction#setEnabled(boolean) + */ + public void setEnabled(boolean enabled) { + item.setEnabled(enabled); + } + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered (through + * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}). + * @see ICommonAction#setRunnable(Runnable) + */ + public void setRunnable(final Runnable runnable) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + runnable.run(); + } + }); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 0000000..8e9e11b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface UiThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 0000000..e767eda --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are not executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface WorkerThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 0000000..4df4376 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.console; + + +/** + * Static Console used to ouput messages. By default outputs the message to System.out and + * System.err, but can receive a IDdmConsole object which will actually do something. + */ +public class DdmConsole { + + private static IDdmConsole mConsole; + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String message) { + if (mConsole != null) { + mConsole.printErrorToConsole(message); + } else { + System.err.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printErrorToConsole(messages); + } else { + for (String message : messages) { + System.err.println(message); + } + } + } + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String message) { + if (mConsole != null) { + mConsole.printToConsole(message); + } else { + System.out.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printToConsole(messages); + } else { + for (String message : messages) { + System.out.println(message); + } + } + } + + /** + * Sets a IDdmConsole to override the default behavior of the console + * @param console The new IDdmConsole + * **/ + public static void setConsole(IDdmConsole console) { + mConsole = console; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 0000000..3679d41 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.console; + + +/** + * DDMS console interface. + */ +public interface IDdmConsole { + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printErrorToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printErrorToConsole(String[] messages); + + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printToConsole(String[] messages); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 0000000..75c19fe --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.FileListingService.IListingReceiver; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; + +/** + * Content provider class for device Explorer. + */ +class DeviceContentProvider implements ITreeContentProvider { + + private TreeViewer mViewer; + private FileListingService mFileListingService; + private FileEntry mRootEntry; + + private IListingReceiver sListingReceiver = new IListingReceiver() { + public void setChildren(final FileEntry entry, FileEntry[] children) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + + // force it open, since on linux and windows + // when getChildren() returns null, the node is + // not considered expanded. + mViewer.setExpandedState(entry, true); + } + } + }); + } + } + } + + public void refreshEntry(final FileEntry entry) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + } + } + }); + } + } + } + }; + + /** + * + */ + public DeviceContentProvider() { + } + + public void setListingService(FileListingService fls) { + mFileListingService = fls; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object) + */ + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof FileEntry) { + FileEntry parentEntry = (FileEntry)parentElement; + + Object[] oldEntries = parentEntry.getCachedChildren(); + Object[] newEntries = mFileListingService.getChildren(parentEntry, + true, sListingReceiver); + + if (newEntries != null) { + return newEntries; + } else { + // if null was returned, this means the cache was not valid, + // and a thread was launched for ls. sListingReceiver will be + // notified with the new entries. + return oldEntries; + } + } + return new Object[0]; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object) + */ + public Object getParent(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getParent(); + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object) + */ + public boolean hasChildren(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getType() == FileListingService.TYPE_DIRECTORY; + } + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object) + */ + public Object[] getElements(Object inputElement) { + if (inputElement instanceof FileEntry) { + FileEntry entry = (FileEntry)inputElement; + if (entry.isRoot()) { + return getChildren(mRootEntry); + } + } + + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) + */ + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (viewer instanceof TreeViewer) { + mViewer = (TreeViewer)viewer; + } + if (newInput instanceof FileEntry) { + mRootEntry = (FileEntry)newInput; + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 0000000..ba0f555 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.explorer; + +import com.android.ddmlib.Device; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.Panel; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.console.DdmConsole; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.ViewerDropAdapter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.FileTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Device filesystem explorer class. + */ +public class DeviceExplorer extends Panel { + + private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S + private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S + + private static Pattern mKeyFilePattern = Pattern.compile( + "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S + private static Pattern mDataFilePattern = Pattern.compile( + "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S + + public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S + public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S + public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S + public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S + public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S + public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S + + private Composite mParent; + private TreeViewer mTreeViewer; + private Tree mTree; + private DeviceContentProvider mContentProvider; + + private ICommonAction mPushAction; + private ICommonAction mPullAction; + private ICommonAction mDeleteAction; + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + private Device mCurrentDevice; + + private String mDefaultSave; + + /** + * Implementation of the SyncService.ISyncProgressMonitor. It wraps a jFace IProgressMonitor + * and just forward the calls to the jFace object. + */ + private static class SyncProgressMonitor implements ISyncProgressMonitor { + + private IProgressMonitor mMonitor; + private String mName; + + SyncProgressMonitor(IProgressMonitor monitor, String name) { + mMonitor = monitor; + mName = name; + } + + public void start(int totalWork) { + mMonitor.beginTask(mName, totalWork); + } + + public void stop() { + mMonitor.done(); + } + + public void advance(int work) { + mMonitor.worked(work); + } + + public boolean isCanceled() { + return mMonitor.isCanceled(); + } + + public void startSubTask(String name) { + mMonitor.subTask(name); + } + } + + public DeviceExplorer() { + + } + + /** + * Sets the images for the listview + * @param fileImage + * @param folderImage + * @param otherImage + */ + public void setImages(Image fileImage, Image folderImage, Image packageImage, + Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mPackageImage = packageImage; + mOtherImage = otherImage; + } + + /** + * Sets the actions so that the device explorer can enable/disable them based on the current + * selection + * @param pushAction + * @param pullAction + * @param deleteAction + */ + public void setActions(ICommonAction pushAction, ICommonAction pullAction, + ICommonAction deleteAction) { + mPushAction = pushAction; + mPullAction = pullAction; + mDeleteAction = deleteAction; + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + parent.setLayout(new FillLayout()); + + mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL); + mTree.setHeaderVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + // create columns + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT, + "000000", COLUMN_SIZE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT, + "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT, + "20:54", COLUMN_TIME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT, + "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT, + "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$ + + // create the jface wrapper + mTreeViewer = new TreeViewer(mTree); + + // setup data provider + mContentProvider = new DeviceContentProvider(); + mTreeViewer.setContentProvider(mContentProvider); + mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage, + mFolderImage, mPackageImage, mOtherImage)); + + // setup a listener for selection + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection sel = event.getSelection(); + if (sel.isEmpty()) { + mPullAction.setEnabled(false); + mPushAction.setEnabled(false); + mDeleteAction.setEnabled(false); + return; + } + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof FileEntry) { + mPullAction.setEnabled(true); + mPushAction.setEnabled(selection.size() == 1); + if (selection.size() == 1) { + setDeleteEnabledState((FileEntry)element); + } else { + mDeleteAction.setEnabled(false); + } + } + } + } + }); + + // add support for double click + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + + if (selection.size() == 1) { + FileEntry entry = (FileEntry)selection.getFirstElement(); + String name = entry.getName(); + + FileEntry parentEntry = entry.getParent(); + + // can't really do anything with no parent + if (parentEntry == null) { + return; + } + + // check this is a file like we want. + Matcher m = mKeyFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the data extension + String dataName = baseName + TRACE_DATA_EXT; + + FileEntry dataEntry = parentEntry.findChild(dataName); + + handleTraceDoubleClick(baseName, entry, dataEntry); + + } else { + m = mDataFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the key extension + String keyName = baseName + TRACE_KEY_EXT; + + FileEntry keyEntry = parentEntry.findChild(keyName); + + handleTraceDoubleClick(baseName, keyEntry, entry); + } + } + } + } + } + }); + + // setup drop listener + mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, + new Transfer[] { FileTransfer.getInstance() }, + new ViewerDropAdapter(mTreeViewer) { + @Override + public boolean performDrop(Object data) { + // get the item on which we dropped the item(s) + FileEntry target = (FileEntry)getCurrentTarget(); + + // in case we drop at the same level as root + if (target == null) { + return false; + } + + // if the target is not a directory, we get the parent directory + if (target.isDirectory() == false) { + target = target.getParent(); + } + + if (target == null) { + return false; + } + + // get the list of files to drop + String[] files = (String[])data; + + // do the drop + pushFiles(files, target); + + // we need to finish with a refresh + refresh(target); + + return true; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + if (target == null) { + return false; + } + + // convert to the real item + FileEntry targetEntry = (FileEntry)target; + + // if the target is not a directory, we get the parent directory + if (targetEntry.isDirectory() == false) { + target = targetEntry.getParent(); + } + + if (target == null) { + return false; + } + + return true; + } + }); + + // create and start the refresh thread + new Thread("Device Ls refresher") { + @Override + public void run() { + while (true) { + try { + sleep(FileListingService.REFRESH_RATE); + } catch (InterruptedException e) { + return; + } + + if (mTree != null && mTree.isDisposed() == false) { + Display display = mTree.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + mTreeViewer.refresh(true); + } + } + }); + } else { + return; + } + } else { + return; + } + } + + } + }.start(); + + return mTree; + } + + @Override + protected void postCreation() { + + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + /** + * Processes a double click on a trace file + * @param baseName the base name of the 2 files. + * @param keyEntry The FileEntry for the .key file. + * @param dataEntry The FileEntry for the .data file. + */ + private void handleTraceDoubleClick(String baseName, FileEntry keyEntry, + FileEntry dataEntry) { + // first we need to download the files. + File keyFile; + File dataFile; + String path; + try { + // create a temp file for keyFile + File f = File.createTempFile(baseName, ".trace"); + f.delete(); + f.mkdir(); + + path = f.getAbsolutePath(); + + keyFile = new File(path + File.separator + keyEntry.getName()); + dataFile = new File(path + File.separator + dataEntry.getName()); + } catch (IOException e) { + return; + } + + // download the files + SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor(); + SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage())); + return; + } + + result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage())); + return; + } + + // now that we have the file, we need to launch traceview + String[] command = new String[2]; + command[0] = DdmUiPreferences.getTraceview(); + command[1] = path + File.separator + baseName; + + try { + final Process p = Runtime.getRuntime().exec(command); + + // create a thread for the output + new Thread("Traceview output") { + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(p.getErrorStream()); + BufferedReader resultReader = new BufferedReader(is); + + // read the lines as they come. if null is returned, it's + // because the process finished + try { + while (true) { + String line = resultReader.readLine(); + if (line != null) { + DdmConsole.printErrorToConsole("Traceview: " + line); + } else { + break; + } + } + // get the return code from the process + p.waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + + } + } + }.start(); + + } catch (IOException e) { + } + } + } + + /** + * Pull the current selection on the local drive. This method displays + * a dialog box to let the user select where to store the file(s) and + * folder(s). + */ + public void pullSelection() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + // name of the single file pull, or null if we're pulling a directory + // or more than one object. + String filePullName = null; + FileEntry singleEntry = null; + + // are we pulling a single file? + if (items.length == 1) { + singleEntry = (FileEntry)items[0].getData(); + if (singleEntry.getType() == FileListingService.TYPE_FILE) { + filePullName = singleEntry.getName(); + } + } + + // where do we save by default? + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + + if (filePullName != null) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Get Device File"); + fileDialog.setFileName(filePullName); + fileDialog.setFilterPath(defaultPath); + + String fileName = fileDialog.open(); + if (fileName != null) { + mDefaultSave = fileDialog.getFilterPath(); + + pullFile(singleEntry, fileName); + } + } else { + DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE); + + directoryDialog.setText("Get Device Files/Folders"); + directoryDialog.setFilterPath(defaultPath); + + String directoryName = directoryDialog.open(); + if (directoryName != null) { + pullSelection(items, directoryName); + } + } + } + + /** + * Push new file(s) and folder(s) into the current selection. Current + * selection must be single item. If the current selection is not a + * directory, the parent directory is used. + * This method displays a dialog to let the user choose file to push to + * the device. + */ + public void pushIntoSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length == 0) { + return; + } + + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN); + String fileName; + + dlg.setText("Put File on Device"); + + // There should be only one. + FileEntry entry = (FileEntry)items[0].getData(); + dlg.setFileName(entry.getName()); + + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultSave = dlg.getFilterPath(); + + // we need to figure out the remote path based on the current selection type. + String remotePath; + FileEntry toRefresh = entry; + if (entry.isDirectory()) { + remotePath = entry.getFullPath(); + } else { + toRefresh = entry.getParent(); + remotePath = toRefresh.getFullPath(); + } + + pushFile(fileName, remotePath); + mTreeViewer.refresh(toRefresh); + } + } + + public void deleteSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + FileEntry entry = (FileEntry)items[0].getData(); + final FileEntry parentEntry = entry.getParent(); + + // create the delete command + String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$ + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + public void addOutput(byte[] data, int offset, int length) { + // pass + // TODO get output to display errors if any. + } + + public void flush() { + mTreeViewer.refresh(parentEntry); + } + + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } + + } + + /** + * Force a full refresh of the explorer. + */ + public void refresh() { + mTreeViewer.refresh(true); + } + + /** + * Sets the new device to explorer + */ + public void switchDevice(final Device device) { + if (device != mCurrentDevice) { + mCurrentDevice = device; + // now we change the input. but we need to do that in the + // ui thread. + if (mTree.isDisposed() == false) { + Display d = mTree.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // new service + if (mCurrentDevice != null) { + FileListingService fls = mCurrentDevice.getFileListingService(); + mContentProvider.setListingService(fls); + mTreeViewer.setInput(fls.getRoot()); + } + } + } + }); + } + } + } + + /** + * Refresh an entry from a non ui thread. + * @param entry the entry to refresh. + */ + private void refresh(final FileEntry entry) { + Display d = mTreeViewer.getTree().getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + mTreeViewer.refresh(entry); + } + }); + } + + /** + * Pulls the selection from a device. + * @param items the tree selection the remote file on the device + * @param localDirector the local directory in which to save the files. + */ + private void pullSelection(TreeItem[] items, final String localDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + // make a list of the FileEntry. + ArrayList<FileEntry> entries = new ArrayList<FileEntry>(); + for (TreeItem item : items) { + Object data = item.getData(); + if (data instanceof FileEntry) { + entries.add((FileEntry)data); + } + } + final FileEntry[] entryArray = entries.toArray( + new FileEntry[entries.size()]); + + // get a progressdialog + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // create a monitor wrapper around the jface monitor + SyncResult result = sync.pull(entryArray, localDirectory, + new SyncProgressMonitor(monitor, + "Pulling file(s) from the device")); + + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", result.getMessage())); + } + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + } + + /** + * Pulls a file from a device. + * @param remote the remote file on the device + * @param local the destination filepath + */ + private void pullFile(final FileEntry remote, final String local) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor( + monitor, String.format("Pulling %1$s from the device", + remote.getName()))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", remote, result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + } + + /** + * Pushes several files and directory into a remote directory. + * @param localFiles + * @param remoteDirectory + */ + private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.push(localFiles, remoteDirectory, + new SyncProgressMonitor(monitor, + "Pushing file(s) to the device")); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push the items: %1$s", result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + return; + } + } + + /** + * Pushes a file on a device. + * @param local the local filepath of the file to push + * @param remoteDirectory the remote destination directory on the device + */ + private void pushFile(final String local, final String remoteDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // get the file name + String[] segs = local.split(Pattern.quote(File.separator)); + String name = segs[segs.length-1]; + String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + + name; + + SyncResult result = sync.pushFile(local, remoteFile, + new SyncProgressMonitor(monitor, + String.format("Pushing %1$s to the device.", name))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push %1$s on %2$s: %3$s", + name, mCurrentDevice.getSerialNumber(), result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } + return; + } + } + + /** + * Sets the enabled state based on a FileEntry properties + * @param element The selected FileEntry + */ + protected void setDeleteEnabledState(FileEntry element) { + mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 0000000..1dca962 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; + +/** + * Label provider for the FileEntry. + */ +class FileLabelProvider implements ILabelProvider, ITableLabelProvider { + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + /** + * Creates Label provider with custom images. + * @param fileImage the Image to represent a file + * @param folderImage the Image to represent a folder + * @param packageImage the Image to represent a .apk file. If null, + * fileImage is used instead. + * @param otherImage the Image to represent all other entry type. + */ + public FileLabelProvider(Image fileImage, Image folderImage, + Image packageImage, Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mOtherImage = otherImage; + if (packageImage != null) { + mPackageImage = packageImage; + } else { + mPackageImage = fileImage; + } + } + + /** + * Creates a label provider with default images. + * + */ + public FileLabelProvider() { + + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object) + */ + public Image getImage(Object element) { + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object) + */ + public String getText(Object element) { + return null; + } + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == 0) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + switch (entry.getType()) { + case FileListingService.TYPE_FILE: + case FileListingService.TYPE_LINK: + // get the name and extension + if (entry.isApplicationPackage()) { + return mPackageImage; + } + return mFileImage; + case FileListingService.TYPE_DIRECTORY: + case FileListingService.TYPE_DIRECTORY_LINK: + return mFolderImage; + } + } + + // default case return a different image. + return mOtherImage; + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + switch (columnIndex) { + case 0: + return entry.getName(); + case 1: + return entry.getSize(); + case 2: + return entry.getDate(); + case 3: + return entry.getTime(); + case 4: + return entry.getPermissions(); + case 5: + return entry.getInfo(); + } + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void addListener(ILabelProviderListener listener) { + // we don't need listeners. + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String) + */ + public boolean isLabelProperty(Object element, String property) { + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void removeListener(ILabelProviderListener listener) { + // we don't need listeners + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java new file mode 100644 index 0000000..578a7ac --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; + +/** + * Encapsulation of controls handling a location coordinate in decimal and sexagesimal. + * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener} + * on all the {@link Text} widgets. + * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by + * a call to {@link #isValueValid()}) + */ +public final class CoordinateControls { + private double mValue; + private boolean mValueValidity = false; + private Text mDecimalText; + private Text mSexagesimalDegreeText; + private Text mSexagesimalMinuteText; + private Text mSexagesimalSecondText; + + /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)} + * is called. This is an int instead of a boolean to act as a counter. */ + private int mManualTextChange = 0; + + /** + * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode. + */ + private ModifyListener mSexagesimalListener = new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = getValueFromSexagesimalControls(); + setValueIntoDecimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the decimal controls. + mValueValidity = false; + resetDecimalControls(); + } + } + }; + + /** + * Creates the {@link Text} control for the decimal display of the coordinate. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createDecimalText(Composite parent) { + mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = Double.parseDouble(mDecimalText.getText()); + setValueIntoSexagesimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the sexagesimal controls. + mValueValidity = false; + resetSexagesimalControls(); + } + } + }); + } + + /** + * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalDegreeText(Composite parent) { + mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalMinuteText(Composite parent) { + mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalSecondText(Composite parent) { + mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Sets the coordinate into the {@link Text} controls. + * @param value the coordinate value to set. + */ + public void setValue(double value) { + mValue = value; + mValueValidity = true; + setValueIntoDecimalControl(value); + setValueIntoSexagesimalControl(value); + } + + /** + * Returns whether the value in the control(s) is valid. + */ + public boolean isValueValid() { + return mValueValidity; + } + + /** + * Returns the current value set in the control(s). + * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed + * before any call to this method. + */ + public double getValue() { + return mValue; + } + + /** + * Enables or disables all the {@link Text} controls. + * @param enabled the enabled state. + */ + public void setEnabled(boolean enabled) { + mDecimalText.setEnabled(enabled); + mSexagesimalDegreeText.setEnabled(enabled); + mSexagesimalMinuteText.setEnabled(enabled); + mSexagesimalSecondText.setEnabled(enabled); + } + + private void resetDecimalControls() { + mManualTextChange++; + mDecimalText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + private void resetSexagesimalControls() { + mManualTextChange++; + mSexagesimalDegreeText.setText(""); //$NON-NLS-1$ + mSexagesimalMinuteText.setText(""); //$NON-NLS-1$ + mSexagesimalSecondText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + /** + * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener} + * @param parent the parent {@link Composite}. + * @param defaultString the default string to be used to compute the {@link Text} control + * size hint. + * @param listener the {@link ModifyListener} to be called when the {@link Text} control is + * modified. + */ + private Text createTextControl(Composite parent, String defaultString, + ModifyListener listener) { + // create the control + Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + + // add the standard listener to it. + text.addModifyListener(listener); + + // compute its size/ + mManualTextChange++; + text.setText(defaultString); + text.pack(); + Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT); + text.setText(""); //$NON-NLS-1$ + mManualTextChange--; + + GridData gridData = new GridData(); + gridData.widthHint = size.x; + text.setLayoutData(gridData); + + return text; + } + + private double getValueFromSexagesimalControls() throws NumberFormatException { + double degrees = Double.parseDouble(mSexagesimalDegreeText.getText()); + double minutes = Double.parseDouble(mSexagesimalMinuteText.getText()); + double seconds = Double.parseDouble(mSexagesimalSecondText.getText()); + + boolean isPositive = (degrees >= 0.); + degrees = Math.abs(degrees); + + double value = degrees + minutes / 60. + seconds / 3600.; + return isPositive ? value : - value; + } + + private void setValueIntoDecimalControl(double value) { + mManualTextChange++; + mDecimalText.setText(String.format("%.6f", value)); + mManualTextChange--; + } + + private void setValueIntoSexagesimalControl(double value) { + // get the sign and make the number positive no matter what. + boolean isPositive = (value >= 0.); + value = Math.abs(value); + + // get the degree + double degrees = Math.floor(value); + + // get the minutes + double minutes = Math.floor((value - degrees) * 60.); + + // get the seconds. + double seconds = (value - degrees) * 3600. - minutes * 60.; + + mManualTextChange++; + mSexagesimalDegreeText.setText( + Integer.toString(isPositive ? (int)degrees : (int)- degrees)); + mSexagesimalMinuteText.setText(Integer.toString((int)minutes)); + mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$ + mManualTextChange--; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 0000000..a30337a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic GPX parser to meet the need of the emulator control panel. + * <p/> + * It parses basic waypoint information, and tracks (merging segments). + */ +public class GpxParser { + + private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$ + + private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$ + private final static String NODE_TRACK = "trk"; //$NON-NLS-1$ + private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$ + private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_TIME = "time"; //$NON-NLS-1$ + private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$ + private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$ + private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$ + private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$ + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private GpxHandler mHandler; + + /** Pattern to parse time with optional sub-second precision, and optional + * Z indicating the time is in UTC. */ + private final static Pattern ISO8601_TIME = + Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$ + + /** + * Handler for the SAX parser. + */ + private static class GpxHandler extends DefaultHandler { + // --------- parsed data --------- + List<WayPoint> mWayPoints; + List<Track> mTrackList; + + // --------- state for parsing --------- + Track mCurrentTrack; + TrackPoint mCurrentTrackPoint; + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList<WayPoint>(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + handleLocation(mCurrentWayPoint, attributes); + } else if (NODE_TRACK.equals(localName)) { + if (mTrackList == null) { + mTrackList = new ArrayList<Track>(); + } + + mTrackList.add(mCurrentTrack = new Track()); + } else if (NODE_TRACK_SEGMENT.equals(localName)) { + // for now we do nothing here. This will merge all the segments into + // a single TrackPoint list in the Track. + } else if (NODE_TRACK_POINT.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint()); + handleLocation(mCurrentTrackPoint, attributes); + } + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_TRACK.equals(localName)) { + mCurrentTrack = null; + } else if (NODE_TRACK_POINT.equals(localName)) { + mCurrentTrackPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.setName(mStringAccumulator.toString()); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_TIME.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString())); + } + } else if (NODE_ELEVATION.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } + } else if (NODE_DESCRIPTION.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setDescription(mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Converts the string description of the time into milliseconds since epoch. + * @param timeString the string data. + * @return date in milliseconds. + */ + private long computeTime(String timeString) { + // Time looks like: 2008-04-05T19:24:50Z + Matcher m = ISO8601_TIME.matcher(timeString); + if (m.matches()) { + // get the various elements and reconstruct time as a long. + try { + int year = Integer.parseInt(m.group(1)); + int month = Integer.parseInt(m.group(2)); + int date = Integer.parseInt(m.group(3)); + int hourOfDay = Integer.parseInt(m.group(4)); + int minute = Integer.parseInt(m.group(5)); + int second = Integer.parseInt(m.group(6)); + + // handle the optional parameters. + int milliseconds = 0; + + String subSecondGroup = m.group(7); + if (subSecondGroup != null) { + milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup)); + } + + boolean utcTime = m.group(8) != null; + + // now we convert into milliseconds since epoch. + Calendar c; + if (utcTime) { + c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ + } else { + c = Calendar.getInstance(); + } + + c.set(year, month, date, hourOfDay, minute, second); + + return c.getTimeInMillis() + milliseconds; + } catch (NumberFormatException e) { + // format is invalid, we'll return -1 below. + } + + } + + // invalid time! + return -1; + } + + /** + * Handles the location attributes and store them into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param attributes the attributes from the XML node. + */ + private void handleLocation(LocationPoint locationNode, Attributes attributes) { + try { + double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE)); + double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE)); + + locationNode.setLocation(longitude, latitude); + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + Track[] getTracks() { + if (mTrackList != null) { + return mTrackList.toArray(new Track[mTrackList.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * A GPS track. + * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment. + */ + public final static class Track { + private String mName; + private String mComment; + private List<TrackPoint> mPoints = new ArrayList<TrackPoint>(); + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setComment(String comment) { + mComment = comment; + } + + public String getComment() { + return mComment; + } + + void addPoint(TrackPoint trackPoint) { + mPoints.add(trackPoint); + } + + public TrackPoint[] getPoints() { + return mPoints.toArray(new TrackPoint[mPoints.size()]); + } + + public long getFirstPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(0).getTime(); + } + + return -1; + } + + public long getLastPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(mPoints.size()-1).getTime(); + } + + return -1; + } + + public int getPointCount() { + return mPoints.size(); + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public GpxParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return <code>true</code> if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new GpxHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } + + /** + * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public Track[] getTracks() { + if (mHandler != null) { + return mHandler.getTracks(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 0000000..af485ac --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic KML parser to meet the need of the emulator control panel. + * <p/> + * It parses basic Placemark information. + */ +public class KmlParser { + + private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$ + + private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$ + + private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?"); + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private KmlHandler mHandler; + + /** + * Handler for the SAX parser. + */ + private static class KmlHandler extends DefaultHandler { + // --------- parsed data --------- + List<WayPoint> mWayPoints; + + // --------- state for parsing --------- + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList<WayPoint>(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_COORDINATES.equals(localName)) { + if (mCurrentWayPoint != null) { + parseLocation(mCurrentWayPoint, mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Parses the location string and store the information into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param location The string containing the location info. + */ + private void parseLocation(LocationPoint locationNode, String location) { + Matcher m = sLocationPattern.matcher(location); + if (m.matches()) { + try { + double longitude = Double.parseDouble(m.group(1)); + double latitude = Double.parseDouble(m.group(2)); + + locationNode.setLocation(longitude, latitude); + + if (m.groupCount() == 3) { + // looks like we have elevation data. + locationNode.setElevation(Double.parseDouble(m.group(3))); + } + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public KmlParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return <code>true</code> if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new KmlHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java new file mode 100644 index 0000000..dbb8f41 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +/** + * Base class for Location aware points. + */ +class LocationPoint { + private double mLongitude; + private double mLatitude; + private boolean mHasElevation = false; + private double mElevation; + + final void setLocation(double longitude, double latitude) { + mLongitude = longitude; + mLatitude = latitude; + } + + public final double getLongitude() { + return mLongitude; + } + + public final double getLatitude() { + return mLatitude; + } + + final void setElevation(double elevation) { + mElevation = elevation; + mHasElevation = true; + } + + public final boolean hasElevation() { + return mHasElevation; + } + + public final double getElevation() { + return mElevation; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java new file mode 100644 index 0000000..7fb37ce --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link Track} objects in a Table. + * <p/>The expected type for the input is {@link Track}<code>[]</code>. + */ +public class TrackContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Track[]) { + return (Track[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 0000000..81d1f7d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Label Provider for {@link Table} objects displaying {@link Track} objects. + */ +public class TrackLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Track) { + Track track = (Track)element; + switch (columnIndex) { + case 0: + return track.getName(); + case 1: + return Integer.toString(track.getPointCount()); + case 2: + long time = track.getFirstPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 3: + time = track.getLastPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 4: + return track.getComment(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 0000000..527f4bf --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + + +/** + * A Track Point. + * <p/>A track point is a point in time and space. + */ +public class TrackPoint extends LocationPoint { + private long mTime; + + void setTime(long time) { + mTime = time; + } + + public long getTime() { + return mTime; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 0000000..32880bd --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +/** + * A GPS/KML way point. + * <p/>A waypoint is a user specified location, with a name and an optional description. + */ +public final class WayPoint extends LocationPoint { + private String mName; + private String mDescription; + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setDescription(String description) { + mDescription = description; + } + + public String getDescription() { + return mDescription; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 0000000..fced777 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link WayPoint} objects in a Table. + * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>. + */ +public class WayPointContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof WayPoint[]) { + return (WayPoint[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 0000000..f5e6f1b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.location; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +/** + * Label Provider for {@link Table} objects displaying {@link WayPoint} objects. + */ +public class WayPointLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)element; + switch (columnIndex) { + case 0: + return wayPoint.getName(); + case 1: + return String.format("%.6f", wayPoint.getLongitude()); + case 2: + return String.format("%.6f", wayPoint.getLatitude()); + case 3: + if (wayPoint.hasElevation()) { + return String.format("%.1f", wayPoint.getElevation()); + } else { + return "-"; + } + case 4: + return wayPoint.getDescription(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 0000000..9de1ac7 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class BugReportImporter { + + private final static String TAG_HEADER = "------ EVENT LOG TAGS ------"; + private final static String LOG_HEADER = "------ EVENT LOG ------"; + private final static String HEADER_TAG = "------"; + + private String[] mTags; + private String[] mLog; + + public BugReportImporter(String filePath) throws FileNotFoundException { + BufferedReader reader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + + try { + String line; + while ((line = reader.readLine()) != null) { + if (TAG_HEADER.equals(line)) { + readTags(reader); + return; + } + } + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + if (LOG_HEADER.equals(line)) { + mTags = content.toArray(new String[content.size()]); + readLog(reader); + return; + } else { + content.add(line); + } + } + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith(HEADER_TAG) == false) { + content.add(line); + } else { + break; + } + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 0000000..473387a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; + +import java.util.ArrayList; + +public class DisplayFilteredLog extends DisplayLog { + + public DisplayFilteredLog(String name) { + super(name); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_FILTERED_LOG; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 0000000..0cffd7e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYAreaRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class DisplayGraph extends EventDisplay { + + public DisplayGraph(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + Collection<TimeSeriesCollection> datasets = mValueTypeDataSetMap.values(); + for (TimeSeriesCollection dataset : datasets) { + dataset.removeAllSeries(); + } + if (mOccurrenceDataSet != null) { + mOccurrenceDataSet.removeAllSeries(); + } + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + String title = getChartTitle(logParser); + return createCompositeChart(parent, logParser, title); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined + * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from + * the two lists. + * <p/>This method is only called when at least one of the descriptor list is non empty. + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + private void updateChart(EventContainer event, EventLogParser logParser, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + Map<Integer, String> tagMap = logParser.getTagMap(); + + Millisecond millisecondTime = null; + long msec = -1; + + // If the event container is a cpu container (tag == 2721), and there is no descriptor + // for the total CPU load, then we do accumulate all the values. + boolean accumulateValues = false; + double accumulatedValue = 0; + + if (event.mTag == 2721) { + accumulateValues = true; + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + accumulateValues &= (descriptor.valueIndex != 0); + } + } + + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + try { + // get the hashmap for this descriptor + HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + mValueDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it + if (timeSeries == null) { + // get the series name + String seriesFullName = null; + String seriesLabel = getSeriesLabel(event, descriptor); + + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + seriesFullName = String.format("%1$s / %2$s", seriesLabel, + descriptor.valueName); + break; + case EVENT_CHECK_SAME_VALUE: + seriesFullName = String.format("%1$s", seriesLabel); + break; + default: + seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, + tagMap.get(descriptor.eventTag), + descriptor.valueName); + break; + } + + // get the data set for this ValueType + TimeSeriesCollection dataset = getValueDataset( + logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] + .getValueType(), + accumulateValues); + + // create the series + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); + } + + dataset.addSeries(timeSeries); + + // add it to the map. + map.put(event.pid, timeSeries); + } + + // update the timeSeries. + + // get the value from the event + double value = event.getValueAsDouble(descriptor.valueIndex); + + // accumulate the values if needed. + if (accumulateValues) { + accumulatedValue += value; + value = accumulatedValue; + } + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, value); + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { + try { + // get the hashmap for this descriptor + HashMap<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + mOcurrenceDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it. + if (timeSeries == null) { + String seriesLabel = getSeriesLabel(event, descriptor); + + String seriesFullName = String.format("[%1$s:%2$s]", + tagMap.get(descriptor.eventTag), seriesLabel); + + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge); + } + + getOccurrenceDataSet().addSeries(timeSeries); + + map.put(event.pid, timeSeries); + } + + // update the series + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + // go through all the series and remove old values. + if (msec != -1 && mMaximumChartItemAge != -1) { + Collection<HashMap<Integer, TimeSeries>> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + } + } + + /** + * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. + * If the data set is not yet created, it is first allocated and set up into the + * {@link org.jfree.chart.JFreeChart} object. + * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. + * @param accumulateValues + */ + private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { + TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); + if (dataset == null) { + // create the data set and store it in the map + dataset = new TimeSeriesCollection(); + mValueTypeDataSetMap.put(type, dataset); + + // create the renderer and configure it depending on the ValueType + AbstractXYItemRenderer renderer; + if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { + renderer = new XYAreaRenderer(); + } else { + XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); + r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); + + renderer = r; + } + + // set both the dataset and the renderer in the plot object. + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, dataset); + xyPlot.setRenderer(mDataSetCount, renderer); + + // put a new axis label, and configure it. + NumberAxis axis = new NumberAxis(type.toString()); + + if (type == EventValueDescription.ValueType.PERCENT) { + // force percent range to be (0,100) fixed. + axis.setAutoRange(false); + axis.setRange(0., 100.); + } + + // for the index, we ignore the occurrence dataset + int count = mDataSetCount; + if (mOccurrenceDataSet != null) { + count--; + } + + xyPlot.setRangeAxis(count, axis); + if ((count % 2) == 0) { + xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); + } else { + xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); + } + + // now we link the dataset and the axis + xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); + + mDataSetCount++; + } + + return dataset; + } + + /** + * Return the series label for this event. This only contains the pid information. + * @param event the {@link EventContainer} + * @param descriptor the {@link OccurrenceDisplayDescriptor} + * @return the series label. + * @throws InvalidTypeException + */ + private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) + throws InvalidTypeException { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid == false) { + return event.getValueAsString(descriptor.seriesValueIndex); + } else { + return String.format("%1$s (%2$d)", + event.getValueAsString(descriptor.seriesValueIndex), event.pid); + } + } + + return Integer.toString(event.pid); + } + + /** + * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not + * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. + */ + private TimeSeriesCollection getOccurrenceDataSet() { + if (mOccurrenceDataSet == null) { + mOccurrenceDataSet = new TimeSeriesCollection(); + + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); + + OccurrenceRenderer renderer = new OccurrenceRenderer(); + renderer.setBaseShapesVisible(false); + xyPlot.setRenderer(mDataSetCount, renderer); + + mDataSetCount++; + } + + return mOccurrenceDataSet; + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_GRAPH; + } + + /** + * Sets the current {@link EventLogParser} object. + */ + @Override + protected void setNewLogParser(EventLogParser logParser) { + if (mChart != null) { + mChart.setTitle(getChartTitle(logParser)); + } + } + /** + * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. + * + * @param logParser the logParser. + * @return the chart title. + */ + private String getChartTitle(EventLogParser logParser) { + if (mValueDescriptors.size() > 0) { + String chartDesc = null; + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + if (logParser != null) { + chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); + } + break; + case EVENT_CHECK_SAME_VALUE: + if (logParser != null) { + chartDesc = String.format("%1$s / %2$s", + logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), + mValueDescriptors.get(0).valueName); + } + break; + } + + if (chartDesc != null) { + return String.format("%1$s - %2$s", mName, chartDesc); + } + } + + return mName; + } +}
\ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 0000000..26296f3 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.TableHelper; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.Calendar; + +public class DisplayLog extends EventDisplay { + public DisplayLog(String name) { + super(name); + } + + private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ + private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ + private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLogTable.removeAll(); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + addToLog(event, logParser); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) { + return createLogUI(parent, listener); + } + + /** + * Adds an {@link EventContainer} to the log. + * + * @param event the event. + * @param logParser the log parser. + */ + private void addToLog(EventContainer event, EventLogParser logParser) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + // get the value description + EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); + if (valueDescription != null) { + for (int i = 0; i < valueDescription.length; i++) { + EventValueDescription description = valueDescription[i]; + try { + String value = event.getValueAsString(i); + + logValue(date, pidName, eventName, description.getName(), value, + description.getEventValueType(), description.getValueType()); + } catch (InvalidTypeException e) { + logValue(date, pidName, eventName, description.getName(), e.getMessage(), + description.getEventValueType(), description.getValueType()); + } + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + } + + /** + * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by + * the list of descriptors. If an event is configured to be displayed by value and occurrence, + * only the values are displayed (as they mark an event occurrence anyway). + * <p/>This method is only called when at least one of the descriptor list is non empty. + * + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + protected void addToLog(EventContainer event, EventLogParser logParser, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + if (valueDescriptors.size() > 0) { + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + logDescriptor(event, descriptor, date, pidName, eventName, logParser); + } + } else { + // we display the event. Since the StringBuilder contains the header (date, event name, + // pid) at this point, there isn't anything else to display. + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + + + /** + * Logs a value in the ui. + * + * @param date + * @param pid + * @param event + * @param valueName + * @param value + * @param eventValueType + * @param valueType + */ + private void logValue(String date, String pid, String event, String valueName, + String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) { + + TableItem item = new TableItem(mLogTable, SWT.NONE); + item.setText(0, date); + item.setText(1, pid); + item.setText(2, event); + item.setText(3, valueName); + item.setText(4, value); + + String type; + if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) { + type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); + } else { + type = eventValueType.toString(); + } + + item.setText(5, type); + } + + /** + * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. + * + * @param event the EventContainer + * @param descriptor the ValueDisplayDescriptor defining which value to display. + * @param date the date of the event in a string. + * @param pidName + * @param eventName + * @param logParser + */ + private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, + String date, String pidName, String eventName, EventLogParser logParser) { + + String value; + try { + value = event.getValueAsString(descriptor.valueIndex); + } catch (InvalidTypeException e) { + value = e.getMessage(); + } + + EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); + + EventValueDescription valueDescription = values[descriptor.valueIndex]; + + logValue(date, pidName, eventName, descriptor.valueName, value, + valueDescription.getEventValueType(), valueDescription.getValueType()); + } + + /** + * Creates the UI for a log display. + * + * @param parent the parent {@link Composite} + * @param listener the {@link ILogColumnListener} to notify on column resize events. + * @return the top Composite of the UI. + */ + private Control createLogUI(Composite parent, final ILogColumnListener listener) { + Composite mainComp = new Composite(parent, SWT.NONE); + GridLayout gl; + mainComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mainComp.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mLogTable = null; + } + }); + + Label l = new Label(mainComp, SWT.CENTER); + l.setText(mName); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | + SWT.BORDER); + mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableColumn col = TableHelper.createTableColumn( + mLogTable, "Time", + SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(0, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "pid", + SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(1, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Event", + SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(2, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Name", + SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(3, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Value", + SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(4, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Type", + SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(5, (TableColumn) source); + } + } + }); + + mLogTable.setHeaderVisible(true); + mLogTable.setLinesVisible(true); + + return mainComp; + } + + /** + * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable). + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + @Override + void resizeColumn(int index, TableColumn sourceColumn) { + if (mLogTable != null) { + TableColumn col = mLogTable.getColumn(index); + if (col != sourceColumn) { + col.setWidth(sourceColumn.getWidth()); + } + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_LOG_ALL; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 0000000..82cc7a4 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.FixedMillisecond; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.util.ShapeUtilities; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class DisplaySync extends SyncCommon { + + // Information to graph for each authority + private TimePeriodValues mDatasetsSync[]; + private List<String> mTooltipsSync[]; + private CustomXYToolTipGenerator mTooltipGenerators[]; + private TimeSeries mDatasetsSyncTickle[]; + + // Dataset of error events to graph + private TimeSeries mDatasetError; + + public DisplaySync(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Status"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + XYBarRenderer br = new XYBarRenderer(); + mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; + mTooltipsSync = new List[NUM_AUTHS]; + mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(0, br); + + XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); + ls.setBaseLinesVisible(false); + mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; + TimeSeriesCollection tsc = new TimeSeriesCollection(); + xyPlot.setDataset(1, tsc); + xyPlot.setRenderer(1, ls); + + mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); + xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); + XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); + errls.setBaseLinesVisible(false); + errls.setSeriesPaint(0, Color.RED); + xyPlot.setRenderer(2, errls); + + for (int i = 0; i < NUM_AUTHS; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + ls.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSync[i]); + mTooltipsSync[i] = new ArrayList<String>(); + mTooltipGenerators[i] = new CustomXYToolTipGenerator(); + br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); + mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); + + mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", + FixedMillisecond.class); + tsc.addSeries(mDatasetsSyncTickle[i]); + ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_TICKLE) { + int auth = getAuth(event.getValueAsString(0)); + if (auth >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Generate the height for an event. + * Height is somewhat arbitrarily the count of "things" that happened + * during the sync. + * When network traffic measurements are available, code should be modified + * to use that instead. + * @param details The details string associated with the event + * @return The height in arbirary units (0-100) + */ + private int getHeightFromDetails(String details) { + if (details == null) { + return 1; // Arbitrary + } + int total = 0; + String parts[] = details.split("[a-zA-Z]"); + for (String part : parts) { + if ("".equals(part)) continue; + total += Integer.parseInt(part); + } + if (total == 0) { + total = 1; + } + return total; + } + + /** + * Generates the tooltips text for an event. + * This method decodes the cryptic details string. + * @param auth The authority associated with the event + * @param details The details string + * @param eventSource server, poll, etc. + * @return The text to display in the tooltips + */ + private String getTextFromDetails(int auth, String details, int eventSource) { + + StringBuffer sb = new StringBuffer(); + sb.append(AUTH_NAMES[auth]).append(": \n"); + + Scanner scanner = new Scanner(details); + Pattern charPat = Pattern.compile("[a-zA-Z]"); + Pattern numPat = Pattern.compile("[0-9]+"); + while (scanner.hasNext()) { + String key = scanner.findInLine(charPat); + int val = Integer.parseInt(scanner.findInLine(numPat)); + if (auth == GMAIL && "M".equals(key)) { + sb.append("messages from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "L".equals(key)) { + sb.append("labels from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "C".equals(key)) { + sb.append("check conversation requests from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "A".equals(key)) { + sb.append("attachments from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "U".equals(key)) { + sb.append("op updates from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "u".equals(key)) { + sb.append("op updates to server: ").append(val).append("\n"); + } else if (auth == GMAIL && "S".equals(key)) { + sb.append("send/receive cycles: ").append(val).append("\n"); + } else if ("Q".equals(key)) { + sb.append("queries to server: ").append(val).append("\n"); + } else if ("E".equals(key)) { + sb.append("entries from server: ").append(val).append("\n"); + } else if ("u".equals(key)) { + sb.append("updates from client: ").append(val).append("\n"); + } else if ("i".equals(key)) { + sb.append("inserts from client: ").append(val).append("\n"); + } else if ("d".equals(key)) { + sb.append("deletes from client: ").append(val).append("\n"); + } else if ("f".equals(key)) { + sb.append("full sync requested\n"); + } else if ("r".equals(key)) { + sb.append("partial sync unavailable\n"); + } else if ("X".equals(key)) { + sb.append("hard error\n"); + } else if ("e".equals(key)) { + sb.append("number of parse exceptions: ").append(val).append("\n"); + } else if ("c".equals(key)) { + sb.append("number of conflicts: ").append(val).append("\n"); + } else if ("a".equals(key)) { + sb.append("number of auth exceptions: ").append(val).append("\n"); + } else if ("D".equals(key)) { + sb.append("too many deletions\n"); + } else if ("R".equals(key)) { + sb.append("too many retries: ").append(val).append("\n"); + } else if ("b".equals(key)) { + sb.append("database error\n"); + } else if ("x".equals(key)) { + sb.append("soft error\n"); + } else if ("l".equals(key)) { + sb.append("sync already in progress\n"); + } else if ("I".equals(key)) { + sb.append("io exception\n"); + } else if (auth == CONTACTS && "p".equals(key)) { + sb.append("photos uploaded from client: ").append(val).append("\n"); + } else if (auth == CONTACTS && "P".equals(key)) { + sb.append("photos downloaded from server: ").append(val).append("\n"); + } else if (auth == CALENDAR && "F".equals(key)) { + sb.append("server refresh\n"); + } else if (auth == CALENDAR && "s".equals(key)) { + sb.append("server diffs fetched\n"); + } else { + sb.append(key).append("=").append(val); + } + } + if (eventSource == 0) { + sb.append("(server)"); + } else if (eventSource == 1) { + sb.append("(local)"); + } else if (eventSource == 2) { + sb.append("(poll)"); + } else if (eventSource == 3) { + sb.append("(user)"); + } + return sb.toString(); + } + + + /** + * Callback to process a sync event. + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (!newEvent) { + // Details arrived for a previous sync event + // Remove event before reinserting. + int lastItem = mDatasetsSync[auth].getItemCount(); + mDatasetsSync[auth].delete(lastItem-1, lastItem-1); + mTooltipsSync[auth].remove(lastItem-1); + } + double height = getHeightFromDetails(details); + height = height / (stopTime - startTime + 1) * 10000; + if (height > 30) { + height = 30; + } + mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height); + mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource)); + mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC; + } +}
\ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 0000000..36d90ce --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.RegularTimePeriod; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class DisplaySyncHistogram extends SyncCommon { + + Map<SimpleTimePeriod, Integer> mTimePeriodMap[]; + + // Information to graph for each authority + private TimePeriodValues mDatasetsSyncHist[]; + + public DisplaySyncHistogram(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Histogram"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + AbstractXYItemRenderer br = new XYBarRenderer(); + mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; + mTimePeriodMap = new HashMap[NUM_AUTHS + 1]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_AUTHS + 1; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSyncHist[i]); + mTimePeriodMap[i] = new HashMap<SimpleTimePeriod, Integer>(); + + } + } + + /** + * Callback to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + auth = ERRORS; + } + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, delta); + } else { + // sync_details arrived for an event that has already been graphed. + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + // Item turns out to be in error, so transfer time from old auth to error. + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, -delta); + addHistEvent(0, ERRORS, delta); + } + } + } + + /** + * Helper to add an event to the data series. + * Also updates error series if appropriate (x or X in details). + * @param stopTime Time event ends + * @param auth Sync authority + * @param value Value to graph for event + */ + private void addHistEvent(long stopTime, int auth, double value) { + SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth); + + // Loop over all datasets to do the stacking. + for (int i = auth; i <= ERRORS; i++) { + addToPeriod(mDatasetsSyncHist, i, hour, value); + } + } + + private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, + double value) { + int index; + if (mTimePeriodMap[auth].containsKey(period)) { + index = mTimePeriodMap[auth].get(period); + double oldValue = tpv[auth].getValue(index).doubleValue(); + tpv[auth].update(index, oldValue + value); + } else { + index = tpv[auth].getItemCount(); + mTimePeriodMap[auth].put(period, index); + tpv[auth].add(period, value); + } + } + + /** + * Creates a multiple-hour time period for the histogram. + * @param time Time in milliseconds. + * @param numHoursWide: should divide into a day. + * @return SimpleTimePeriod covering the number of hours and containing time. + */ + private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { + Date date = new Date(time); + TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; + Calendar calendar = Calendar.getInstance(zone); + calendar.setTime(date); + long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + + calendar.get(Calendar.DAY_OF_YEAR) * 24; + int year = calendar.get(Calendar.YEAR); + hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; + calendar.clear(); + calendar.set(year, 0, 1, 0, 0); // Jan 1 + long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; + return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_HIST; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 0000000..9ce7045 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +public class DisplaySyncPerf extends SyncCommon { + + CustomXYToolTipGenerator mTooltipGenerator; + List mTooltips[]; + + // The series number for each graphed item. + // sync authorities are 0-3 + private static final int DB_QUERY = 4; + private static final int DB_WRITE = 5; + private static final int HTTP_NETWORK = 6; + private static final int HTTP_PROCESSING = 7; + private static final int NUM_SERIES = (HTTP_PROCESSING + 1); + private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "DB Query", "DB Write", "HTTP Response", "HTTP Processing",}; + private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY}; + private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2}; + + // Values from data/etc/event-log-tags + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_HTTP_STATS = 52001; + // op types for EVENT_DB_OPERATION + final int EVENT_DB_QUERY = 0; + final int EVENT_DB_WRITE = 1; + + // Information to graph for each authority + private TimePeriodValues mDatasets[]; + + /** + * TimePeriodValuesCollection that supports Y intervals. This allows the + * creation of "floating" bars, rather than bars rooted to the axis. + */ + class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection { + /** default serial UID */ + private static final long serialVersionUID = 1L; + + private double yheight; + + /** + * Constructs a collection of bars with a fixed Y height. + * + * @param yheight The height of the bars. + */ + YIntervalTimePeriodValuesCollection(double yheight) { + this.yheight = yheight; + } + + /** + * Returns ending Y value that is a fixed amount greater than the starting value. + * + * @param series the series (zero-based index). + * @param item the item (zero-based index). + * @return The ending Y value for the specified series and item. + */ + @Override + public Number getEndY(int series, int item) { + return getY(series, item).doubleValue() + yheight; + } + } + + /** + * Constructs a graph of network and database stats. + * + * @param name The name of this graph in the graph list. + */ + public DisplaySyncPerf(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Performance"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.getRangeAxis().setVisible(false); + mTooltipGenerator = new CustomXYToolTipGenerator(); + mTooltips = new List[NUM_SERIES]; + + XYBarRenderer br = new XYBarRenderer(); + br.setUseYInterval(true); + mDatasets = new TimePeriodValues[NUM_SERIES]; + + TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_SERIES; i++) { + br.setSeriesPaint(i, SERIES_COLORS[i]); + mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]); + tpvc.addSeries(mDatasets[i]); + mTooltips[i] = new ArrayList<String>(); + mTooltipGenerator.addToolTipSeries(mTooltips[i]); + br.setSeriesToolTipGenerator(i, mTooltipGenerator); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_DB_OPERATION) { + // 52000 db_operation (name|3),(op_type|1|5),(time|2|3) + String tip = event.getValueAsString(0); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + int opType = Integer.parseInt(event.getValueAsString(1)); + long duration = Long.parseLong(event.getValueAsString(2)); + + if (opType == EVENT_DB_QUERY) { + mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_QUERY]); + mTooltips[DB_QUERY].add(tip); + } else if (opType == EVENT_DB_WRITE) { + mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_WRITE]); + mTooltips[DB_WRITE].add(tip); + } + } else if (event.mTag == EVENT_HTTP_STATS) { + // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2) + String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) + + ", rx: " + event.getValueAsString(4); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + long netEndTime = endTime - Long.parseLong(event.getValueAsString(2)); + long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1)); + mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime), + SERIES_YCOORD[HTTP_NETWORK]); + mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime), + SERIES_YCOORD[HTTP_PROCESSING]); + mTooltips[HTTP_NETWORK].add(tip); + mTooltips[HTTP_PROCESSING].add(tip); + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback from super.newEvent to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_PERF; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java new file mode 100644 index 0000000..2223a4d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java @@ -0,0 +1,971 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.Log; +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.event.ChartChangeEvent; +import org.jfree.chart.event.ChartChangeEventType; +import org.jfree.chart.event.ChartChangeListener; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Represents a custom display of one or more events. + */ +abstract class EventDisplay { + + private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$ + private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$ + private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$ + private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$ + + private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$ + + public final static int DISPLAY_TYPE_LOG_ALL = 0; + public final static int DISPLAY_TYPE_FILTERED_LOG = 1; + public final static int DISPLAY_TYPE_GRAPH = 2; + public final static int DISPLAY_TYPE_SYNC = 3; + public final static int DISPLAY_TYPE_SYNC_HIST = 4; + public final static int DISPLAY_TYPE_SYNC_PERF = 5; + + private final static int EVENT_CHECK_FAILED = 0; + protected final static int EVENT_CHECK_SAME_TAG = 1; + protected final static int EVENT_CHECK_SAME_VALUE = 2; + + /** + * Creates the appropriate EventDisplay subclass. + * + * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc) + * @param name the name of the display + * @return the created object + */ + public static EventDisplay eventDisplayFactory(int type, String name) { + switch (type) { + case DISPLAY_TYPE_LOG_ALL: + return new DisplayLog(name); + case DISPLAY_TYPE_FILTERED_LOG: + return new DisplayFilteredLog(name); + case DISPLAY_TYPE_SYNC: + return new DisplaySync(name); + case DISPLAY_TYPE_SYNC_HIST: + return new DisplaySyncHistogram(name); + case DISPLAY_TYPE_GRAPH: + return new DisplayGraph(name); + case DISPLAY_TYPE_SYNC_PERF: + return new DisplaySyncPerf(name); + default: + throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$ + } + } + + /** + * Adds event to the display. + * @param event The event + * @param logParser The log parser. + */ + abstract void newEvent(EventContainer event, EventLogParser logParser); + + /** + * Resets the display. + */ + abstract void resetUI(); + + /** + * Gets display type + * + * @return display type as an integer + */ + abstract int getDisplayType(); + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + abstract Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener); + + interface ILogColumnListener { + void columnResized(int index, TableColumn sourceColumn); + } + + /** + * Describes an event to be displayed. + */ + static class OccurrenceDisplayDescriptor { + + int eventTag = -1; + int seriesValueIndex = -1; + boolean includePid = false; + int filterValueIndex = -1; + CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO; + Object filterValue = null; + + OccurrenceDisplayDescriptor() { + } + + OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) { + replaceWith(descriptor); + } + + OccurrenceDisplayDescriptor(int eventTag) { + this.eventTag = eventTag; + } + + OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) { + this.eventTag = eventTag; + this.seriesValueIndex = seriesValueIndex; + } + + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + eventTag = descriptor.eventTag; + seriesValueIndex = descriptor.seriesValueIndex; + includePid = descriptor.includePid; + filterValueIndex = descriptor.filterValueIndex; + filterCompareMethod = descriptor.filterCompareMethod; + filterValue = descriptor.filterValue; + } + + /** + * Loads the descriptor parameter from a storage string. The storage string must have + * been generated with {@link #getStorageString()}. + * + * @param storageString the storage string + */ + final void loadFrom(String storageString) { + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR)); + loadFrom(values, 0); + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + protected int loadFrom(String[] storageStrings, int index) { + eventTag = Integer.parseInt(storageStrings[index++]); + seriesValueIndex = Integer.parseInt(storageStrings[index++]); + includePid = Boolean.parseBoolean(storageStrings[index++]); + filterValueIndex = Integer.parseInt(storageStrings[index++]); + try { + filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]); + } catch (IllegalArgumentException e) { + // if the name does not match any known CompareMethod, we init it to the default one + filterCompareMethod = CompareMethod.EQUAL_TO; + } + String value = storageStrings[index++]; + if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) { + filterValue = EventValueType.getObjectFromStorageString(value); + } + + return index; + } + + /** + * Returns the storage string for the receiver. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + sb.append(eventTag); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(seriesValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(includePid)); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterCompareMethod.name()); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + if (filterValue != null) { + String value = EventValueType.getStorageString(filterValue); + if (value != null) { + sb.append(value); + } else { + sb.append(FILTER_VALUE_NULL); + } + } else { + sb.append(FILTER_VALUE_NULL); + } + + return sb.toString(); + } + } + + /** + * Describes an event value to be displayed. + */ + static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor { + String valueName; + int valueIndex = -1; + + ValueDisplayDescriptor() { + super(); + } + + ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) { + super(); + replaceWith(descriptor); + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) { + super(eventTag); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex, + int seriesValueIndex) { + super(eventTag, seriesValueIndex); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + @Override + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + super.replaceWith(descriptor); + if (descriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor; + valueName = valueDescriptor.valueName; + valueIndex = valueDescriptor.valueIndex; + } + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + @Override + protected int loadFrom(String[] storageStrings, int index) { + index = super.loadFrom(storageStrings, index); + valueName = storageStrings[index++]; + valueIndex = Integer.parseInt(storageStrings[index++]); + return index; + } + + /** + * Returns the storage string for the receiver. + */ + @Override + String getStorageString() { + String superStorage = super.getStorageString(); + + StringBuilder sb = new StringBuilder(); + sb.append(superStorage); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueName); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueIndex); + + return sb.toString(); + } + } + + /* ================== + * Event Display parameters. + * ================== */ + protected String mName; + + private boolean mPidFiltering = false; + + private ArrayList<Integer> mPidFilterList = null; + + protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + /* ================== + * Event Display members for display purpose. + * ================== */ + // chart objects + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap = + new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap = + new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + + /** + * This is a map of (ValueType, dataset) + */ + protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap = + new HashMap<ValueType, TimeSeriesCollection>(); + + protected JFreeChart mChart; + protected TimeSeriesCollection mOccurrenceDataSet; + protected int mDataSetCount; + private ChartComposite mChartComposite; + protected long mMaximumChartItemAge = -1; + protected long mHistWidth = 1; + + // log objects. + protected Table mLogTable; + + /* ================== + * Misc data. + * ================== */ + protected int mValueDescriptorCheck = EVENT_CHECK_FAILED; + + EventDisplay(String name) { + mName = name; + } + + static EventDisplay clone(EventDisplay from) { + EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName()); + ed.mName = from.mName; + ed.mPidFiltering = from.mPidFiltering; + ed.mMaximumChartItemAge = from.mMaximumChartItemAge; + ed.mHistWidth = from.mHistWidth; + + if (from.mPidFilterList != null) { + ed.mPidFilterList = new ArrayList<Integer>(); + ed.mPidFilterList.addAll(from.mPidFilterList); + } + + for (ValueDisplayDescriptor desc : from.mValueDescriptors) { + ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc)); + } + ed.mValueDescriptorCheck = from.mValueDescriptorCheck; + + for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) { + ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); + } + return ed; + } + + /** + * Returns the parameters of the receiver as a single String for storage. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + + sb.append(mName); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDisplayType()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(mPidFiltering)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getPidStorageString()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mValueDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mOccurrenceDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mMaximumChartItemAge); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mHistWidth); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + + return sb.toString(); + } + + void setName(String name) { + mName = name; + } + + String getName() { + return mName; + } + + void setPidFiltering(boolean filterByPid) { + mPidFiltering = filterByPid; + } + + boolean getPidFiltering() { + return mPidFiltering; + } + + void setPidFilterList(ArrayList<Integer> pids) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + mPidFilterList = pids; + } + + ArrayList<Integer> getPidFilterList() { + return mPidFilterList; + } + + void addPidFiler(int pid) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + if (mPidFilterList == null) { + mPidFilterList = new ArrayList<Integer>(); + } + + mPidFilterList.add(pid); + } + + /** + * Returns an iterator to the list of {@link ValueDisplayDescriptor}. + */ + Iterator<ValueDisplayDescriptor> getValueDescriptors() { + return mValueDescriptors.iterator(); + } + + /** + * Update checks on the descriptors. Must be called whenever a descriptor is modified outside + * of this class. + */ + void updateValueDescriptorCheck() { + mValueDescriptorCheck = checkDescriptors(); + } + + /** + * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}. + */ + Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() { + return mOccurrenceDescriptors.iterator(); + } + + /** + * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a + * {@link ValueDisplayDescriptor}. + * + * @param descriptor the descriptor to be added. + */ + void addDescriptor(OccurrenceDisplayDescriptor descriptor) { + if (descriptor instanceof ValueDisplayDescriptor) { + mValueDescriptors.add((ValueDisplayDescriptor) descriptor); + mValueDescriptorCheck = checkDescriptors(); + } else { + mOccurrenceDescriptors.add(descriptor); + } + } + + /** + * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}). + * + * @param descriptorClass the class of the descriptor to return. + * @param index the index of the descriptor to return. + * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor} + * or <code>null</code> if <code>descriptorClass</code> is another class. + */ + OccurrenceDisplayDescriptor getDescriptor( + Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) { + + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + return mOccurrenceDescriptors.get(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + return mValueDescriptors.get(index); + } + + return null; + } + + /** + * Removes a descriptor based on its class and index. + * + * @param descriptorClass the class of the descriptor. + * @param index the index of the descriptor to be removed. + */ + void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) { + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + mOccurrenceDescriptors.remove(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + mValueDescriptors.remove(index); + mValueDescriptorCheck = checkDescriptors(); + } + } + + Control createCompositeChart(final Composite parent, EventLogParser logParser, + String title) { + mChart = ChartFactory.createTimeSeriesChart( + null, + null /* timeAxisLabel */, + null /* valueAxisLabel */, + null, /* dataset. set below */ + true /* legend */, + false /* tooltips */, + false /* urls */); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = parent.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), + firstFontData, true /* ensureSameSize */); + + + mChart.setTitle(new TextTitle(title, awtFont)); + + final XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setRangeCrosshairVisible(true); + xyPlot.setRangeCrosshairLockedOnData(true); + xyPlot.setDomainCrosshairVisible(true); + xyPlot.setDomainCrosshairLockedOnData(true); + + mChart.addChangeListener(new ChartChangeListener() { + public void chartChanged(ChartChangeEvent event) { + ChartChangeEventType type = event.getType(); + if (type == ChartChangeEventType.GENERAL) { + // because the value we need (rangeCrosshair and domainCrosshair) are + // updated on the draw, but the notification happens before the draw, + // we process the click in a future runnable! + parent.getDisplay().asyncExec(new Runnable() { + public void run() { + processClick(xyPlot); + } + }); + } + } + }); + + mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + true, // zoom + true); // tooltips + + mChartComposite.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mValueTypeDataSetMap.clear(); + mDataSetCount = 0; + mOccurrenceDataSet = null; + mChart = null; + mChartComposite = null; + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + }); + + return mChartComposite; + + } + + private void processClick(XYPlot xyPlot) { + double rangeValue = xyPlot.getRangeCrosshairValue(); + if (rangeValue != 0) { + double domainValue = xyPlot.getDomainCrosshairValue(); + + Millisecond msec = new Millisecond(new Date((long) domainValue)); + + // look for values in the dataset that contains data at this TimePeriod + Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet(); + + for (ValueDisplayDescriptor descKey : descKeys) { + HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey); + + Set<Integer> pidKeys = map.keySet(); + + for (Integer pidKey : pidKeys) { + TimeSeries series = map.get(pidKey); + + Number value = series.getValue(msec); + if (value != null) { + // found a match. lets check against the actual value. + if (value.doubleValue() == rangeValue) { + + return; + } + } + } + } + } + } + + + /** + * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable). + * Subclasses can override if necessary. + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + void resizeColumn(int index, TableColumn sourceColumn) { + } + + /** + * Sets the current {@link EventLogParser} object. + * Subclasses can override if necessary. + */ + protected void setNewLogParser(EventLogParser logParser) { + } + + /** + * Prepares the {@link EventDisplay} for a multi event display. + */ + void startMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(false); + } + } + + /** + * Finalizes the {@link EventDisplay} after a multi event display. + */ + void endMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(true); + } + } + + /** + * Returns the {@link Table} object used to display events, if any. + * + * @return a Table object or <code>null</code>. + */ + Table getTable() { + return mLogTable; + } + + /** + * Loads a new {@link EventDisplay} from a storage string. The string must have been created + * with {@link #getStorageString()}. + * + * @param storageString the storage string + * @return a new {@link EventDisplay} or null if the load failed. + */ + static EventDisplay load(String storageString) { + if (storageString.length() > 0) { + // the storage string is separated by ':' + String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR)); + + try { + int index = 0; + + String name = values[index++]; + int displayType = Integer.parseInt(values[index++]); + boolean pidFiltering = Boolean.parseBoolean(values[index++]); + + EventDisplay ed = eventDisplayFactory(displayType, name); + ed.setPidFiltering(pidFiltering); + + // because empty sections are removed by String.split(), we have to check + // the index for those. + if (index < values.length) { + ed.loadPidFilters(values[index++]); + } + + if (index < values.length) { + ed.loadValueDescriptors(values[index++]); + } + + if (index < values.length) { + ed.loadOccurrenceDescriptors(values[index++]); + } + + ed.updateValueDescriptorCheck(); + + if (index < values.length) { + ed.mMaximumChartItemAge = Long.parseLong(values[index++]); + } + + if (index < values.length) { + ed.mHistWidth = Long.parseLong(values[index++]); + } + + return ed; + } catch (RuntimeException re) { + // we'll return null below. + Log.e("ddms", re); + } + } + + return null; + } + + private String getPidStorageString() { + if (mPidFilterList != null) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Integer i : mPidFilterList) { + if (first == false) { + sb.append(PID_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(i); + } + + return sb.toString(); + } + return ""; //$NON-NLS-1$ + } + + + private void loadPidFilters(String storageString) { + if (storageString.length() > 0) { + String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR)); + + for (String value : values) { + if (mPidFilterList == null) { + mPidFilterList = new ArrayList<Integer>(); + } + mPidFilterList.add(Integer.parseInt(value)); + } + } + } + + private String getDescriptorStorageString( + ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + + for (OccurrenceDisplayDescriptor descriptor : descriptorList) { + if (first == false) { + sb.append(DESCRIPTOR_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(descriptor.getStorageString()); + } + + return sb.toString(); + } + + private void loadOccurrenceDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor(); + desc.loadFrom(value); + mOccurrenceDescriptors.add(desc); + } + } + + private void loadValueDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + ValueDisplayDescriptor desc = new ValueDisplayDescriptor(); + desc.loadFrom(value); + mValueDescriptors.add(desc); + } + } + + /** + * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another + * list if they are configured to display the {@link EventContainer} + * + * @param event the event container + * @param fullList the list with all the descriptors. + * @param outList the list to fill. + */ + @SuppressWarnings("unchecked") + private void getDescriptors(EventContainer event, + ArrayList<? extends OccurrenceDisplayDescriptor> fullList, + ArrayList outList) { + for (OccurrenceDisplayDescriptor descriptor : fullList) { + try { + // first check the event tag. + if (descriptor.eventTag == event.mTag) { + // now check if we have a filter on a value + if (descriptor.filterValueIndex == -1 || + event.testValue(descriptor.filterValueIndex, descriptor.filterValue, + descriptor.filterCompareMethod)) { + outList.add(descriptor); + } + } + } catch (InvalidTypeException ite) { + // if the filter for the descriptor was incorrect, we ignore the descriptor. + } catch (ArrayIndexOutOfBoundsException aioobe) { + // if the index was wrong (the event content may have changed since we setup the + // display), we do nothing but log the error + Log.e("Event Log", String.format( + "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$ + descriptor.filterValueIndex, descriptor.eventTag)); + } + } + } + + /** + * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor} + * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event. + * + * @param event + * @param valueDescriptors + * @param occurrenceDescriptors + * @return true if the event should be displayed. + */ + + protected boolean filterEvent(EventContainer event, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + + // test the pid first (if needed) + if (mPidFiltering && mPidFilterList != null) { + boolean found = false; + for (int pid : mPidFilterList) { + if (pid == event.pid) { + found = true; + break; + } + } + + if (found == false) { + return false; + } + } + + // now get the list of matching descriptors + getDescriptors(event, mValueDescriptors, valueDescriptors); + getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors); + + // and return whether there is at least one match in either list. + return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0); + } + + /** + * Checks all the {@link ValueDisplayDescriptor} for similarity. + * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG. + * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE + * + * @return flag as described above + */ + private int checkDescriptors() { + if (mValueDescriptors.size() < 2) { + return EVENT_CHECK_SAME_VALUE; + } + + int tag = -1; + int index = -1; + for (ValueDisplayDescriptor display : mValueDescriptors) { + if (tag == -1) { + tag = display.eventTag; + index = display.valueIndex; + } else { + if (tag != display.eventTag) { + return EVENT_CHECK_FAILED; + } else { + if (index != -1) { + if (index != display.valueIndex) { + index = -1; + } + } + } + } + } + + if (index == -1) { + return EVENT_CHECK_SAME_TAG; + } + + return EVENT_CHECK_SAME_VALUE; + } + + /** + * Resets the time limit on the chart to be infinite. + */ + void resetChartTimeLimit() { + mMaximumChartItemAge = -1; + } + + /** + * Sets the time limit on the charts. + * + * @param timeLimit the time limit in seconds. + */ + void setChartTimeLimit(long timeLimit) { + mMaximumChartItemAge = timeLimit; + } + + long getChartTimeLimit() { + return mMaximumChartItemAge; + } + + /** + * m + * Resets the histogram width + */ + void resetHistWidth() { + mHistWidth = 1; + } + + /** + * Sets the histogram width + * + * @param histWidth the width in hours + */ + void setHistWidth(long histWidth) { + mHistWidth = histWidth; + } + + long getHistWidth() { + return mHistWidth; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java new file mode 100644 index 0000000..88c3cb2 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -0,0 +1,955 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; + +class EventDisplayOptions extends Dialog { + private static final int DLG_WIDTH = 700; + private static final int DLG_HEIGHT = 700; + + private IImageLoader mImageLoader; + + private Shell mParent; + private Shell mShell; + + private boolean mEditStatus = false; + private final ArrayList<EventDisplay> mDisplayList = new ArrayList<EventDisplay>(); + + /* LEFT LIST */ + private List mEventDisplayList; + private Button mEventDisplayNewButton; + private Button mEventDisplayDeleteButton; + private Button mEventDisplayUpButton; + private Button mEventDisplayDownButton; + private Text mDisplayWidthText; + private Text mDisplayHeightText; + + /* WIDGETS ON THE RIGHT */ + private Text mDisplayNameText; + private Combo mDisplayTypeCombo; + private Group mChartOptions; + private Group mHistOptions; + private Button mPidFilterCheckBox; + private Text mPidText; + + /** Map with (event-tag, event name) */ + private Map<Integer, String> mEventTagMap; + + /** Map with (event-tag, array of value info for the event) */ + private Map<Integer, EventValueDescription[]> mEventDescriptionMap; + + /** list of current pids */ + private ArrayList<Integer> mPidList; + + private EventLogParser mLogParser; + + private Group mInfoGroup; + + private static class SelectionWidgets { + private List mList; + private Button mNewButton; + private Button mEditButton; + private Button mDeleteButton; + + private void setEnabled(boolean enable) { + mList.setEnabled(enable); + mNewButton.setEnabled(enable); + mEditButton.setEnabled(enable); + mDeleteButton.setEnabled(enable); + } + } + + private SelectionWidgets mValueSelection; + private SelectionWidgets mOccurrenceSelection; + + /** flag to temporarly disable processing of {@link Text} changes, so that + * {@link Text#setText(String)} can be called safely. */ + private boolean mProcessTextChanges = true; + private Text mTimeLimitText; + private Text mHistWidthText; + + EventDisplayOptions(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + /** + * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the + * list. + * @param logParser + * @param displayList + * @param eventList + * @return true if the list of {@link EventDisplay} objects was updated. + */ + boolean open(EventLogParser logParser, ArrayList<EventDisplay> displayList, + ArrayList<EventContainer> eventList) { + mLogParser = logParser; + + if (logParser != null) { + // we need 2 things from the parser. + // the event tag / event name map + mEventTagMap = logParser.getTagMap(); + + // the event info map + mEventDescriptionMap = logParser.getEventInfoMap(); + } + + // make a copy of the EventDisplay list since we'll use working copies. + duplicateEventDisplay(displayList); + + // build a list of pid from the list of events. + buildPidList(eventList); + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + // Set the dialog size. + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + ArrayList<EventDisplay> getEventDisplays() { + return mDisplayList; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(1, true)); + + final Composite topPanel = new Composite(mShell, SWT.NONE); + topPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + topPanel.setLayout(new GridLayout(2, false)); + + // create the tree on the left and the controls on the right. + Composite leftPanel = new Composite(topPanel, SWT.NONE); + Composite rightPanel = new Composite(topPanel, SWT.NONE); + + createLeftPanel(leftPanel); + createRightPanel(rightPanel); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + + Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite bottomButtons = new Composite(mShell, SWT.NONE); + bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout gl; + bottomButtons.setLayout(gl = new GridLayout(2, true)); + gl.marginHeight = gl.marginWidth = 0; + + Button okButton = new Button(bottomButtons, SWT.PUSH); + okButton.setText("OK"); + okButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + Button cancelButton = new Button(bottomButtons, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the modification flag. + mEditStatus = false; + + // and close + mShell.close(); + } + }); + + enable(false); + + // fill the list with the current display + fillEventDisplayList(); + } + + private void createLeftPanel(Composite leftPanel) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + GridLayout gl; + + leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + leftPanel.setLayout(gl = new GridLayout(1, false)); + gl.verticalSpacing = 1; + + mEventDisplayList = new List(leftPanel, + SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION); + mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH)); + mEventDisplayList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + handleEventDisplaySelection(); + } + }); + + Composite bottomControls = new Composite(leftPanel, SWT.NONE); + bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + bottomControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayNewButton.setImage(mImageLoader.loadImage("add.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayNewButton.setToolTipText("Adds a new event display"); + mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createNewEventDisplay(); + } + }); + + mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDeleteButton.setImage(mImageLoader.loadImage("delete.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display"); + mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + deleteEventDisplay(); + } + }); + + mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayUpButton.setImage(mImageLoader.loadImage("up.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayUpButton.setToolTipText("Moves the selected event display up"); + mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection > 0) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection - 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection - 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection - 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDownButton.setImage(mImageLoader.loadImage("down.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDownButton.setToolTipText("Moves the selected event display down"); + mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection + 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection + 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection + 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + Group sizeGroup = new Group(leftPanel, SWT.NONE); + sizeGroup.setText("Display Size:"); + sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sizeGroup.setLayout(new GridLayout(2, false)); + + Label l = new Label(sizeGroup, SWT.NONE); + l.setText("Width:"); + + mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayWidthText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH))); + mDisplayWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayWidthText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + + l = new Label(sizeGroup, SWT.NONE); + l.setText("Height:"); + + mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayHeightText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT))); + mDisplayHeightText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayHeightText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + } + + private void createRightPanel(Composite rightPanel) { + rightPanel.setLayout(new GridLayout(1, true)); + rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mInfoGroup = new Group(rightPanel, SWT.NONE); + mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mInfoGroup.setLayout(new GridLayout(2, false)); + + Label nameLabel = new Label(mInfoGroup, SWT.LEFT); + nameLabel.setText("Name:"); + + mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setName(mDisplayNameText.getText()); + int index = mEventDisplayList.getSelectionIndex(); + mEventDisplayList.remove(index); + mEventDisplayList.add(eventDisplay.getName(), index); + mEventDisplayList.select(index); + handleEventDisplaySelection(); + setModified(); + } + } + } + }); + + Label displayLabel = new Label(mInfoGroup, SWT.LEFT); + displayLabel.setText("Type:"); + + mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_* + mDisplayTypeCombo.add("Log All"); + mDisplayTypeCombo.add("Filtered Log"); + mDisplayTypeCombo.add("Graph"); + mDisplayTypeCombo.add("Sync"); + mDisplayTypeCombo.add("Sync Histogram"); + mDisplayTypeCombo.add("Sync Performance"); + mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) { + /* Replace the EventDisplay object with a different subclass */ + setModified(); + String name = eventDisplay.getName(); + EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name); + setCurrentEventDisplay(newEventDisplay); + fillUiWith(newEventDisplay); + } + } + }); + + mChartOptions = new Group(mInfoGroup, SWT.NONE); + mChartOptions.setText("Chart Options"); + GridData gd; + mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mChartOptions.setLayout(new GridLayout(2, false)); + + Label l = new Label(mChartOptions, SWT.NONE); + l.setText("Time Limit (seconds):"); + + mTimeLimitText = new Text(mChartOptions, SWT.BORDER); + mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTimeLimitText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mTimeLimitText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetChartTimeLimit(); + } else { + eventDisplay.setChartTimeLimit(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetChartTimeLimit(); + } finally { + setModified(); + } + } + } + }); + + mHistOptions = new Group(mInfoGroup, SWT.NONE); + mHistOptions.setText("Histogram Options"); + GridData gdh; + mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL)); + gdh.horizontalSpan = 2; + mHistOptions.setLayout(new GridLayout(2, false)); + + Label lh = new Label(mHistOptions, SWT.NONE); + lh.setText("Histogram width (hours):"); + + mHistWidthText = new Text(mHistOptions, SWT.BORDER); + mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mHistWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mHistWidthText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetHistWidth(); + } else { + eventDisplay.setHistWidth(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetHistWidth(); + } finally { + setModified(); + } + } + } + }); + + mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK); + mPidFilterCheckBox.setText("Enable filtering by pid"); + mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection()); + mPidText.setEnabled(mPidFilterCheckBox.getSelection()); + setModified(); + } + } + }); + + Label pidLabel = new Label(mInfoGroup, SWT.NONE); + pidLabel.setText("Pid Filter:"); + pidLabel.setToolTipText("Enter all pids, separated by commas"); + + mPidText = new Text(mInfoGroup, SWT.BORDER); + mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getPidFiltering()) { + String pidText = mPidText.getText().trim(); + String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList<Integer> list = new ArrayList<Integer>(); + for (String pid : pids) { + try { + list.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // just ignore non valid pid + } + } + + eventDisplay.setPidFilterList(list); + setModified(); + } + } + } + }); + + /* ------------------ + * EVENT VALUE/OCCURRENCE SELECTION + * ------------------ */ + mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class, + "Event Value Display"); + mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class, + "Event Occurrence Display"); + } + + private SelectionWidgets createEventSelection(Composite rightPanel, + final Class<? extends OccurrenceDisplayDescriptor> descriptorClass, + String groupMessage) { + + Group eventSelectionPanel = new Group(rightPanel, SWT.NONE); + eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl; + eventSelectionPanel.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + eventSelectionPanel.setText(groupMessage); + + final SelectionWidgets widgets = new SelectionWidgets(); + + widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL); + widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH)); + widgets.mList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + widgets.mDeleteButton.setEnabled(true); + widgets.mEditButton.setEnabled(true); + } else { + widgets.mDeleteButton.setEnabled(false); + widgets.mEditButton.setEnabled(false); + } + } + }); + + Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE); + rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + rightControls.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mNewButton.setText("New..."); + widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mNewButton.setEnabled(false); + widgets.mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + try { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptorClass, mLogParser)) { + eventDisplay.addDescriptor(dialog.getDescriptor()); + fillUiWith(eventDisplay); + setModified(); + } + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + }); + + widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mEditButton.setText("Edit..."); + widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mEditButton.setEnabled(false); + widgets.mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + // get the descriptor itself + OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor( + descriptorClass, index); + + // open the edit dialog. + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptor, mLogParser)) { + descriptor.replaceWith(dialog.getDescriptor()); + eventDisplay.updateValueDescriptorCheck(); + fillUiWith(eventDisplay); + + // reselect the item since fillUiWith remove the selection. + widgets.mList.select(index); + widgets.mList.notifyListeners(SWT.Selection, null); + + setModified(); + } + } + } + } + }); + + widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mDeleteButton.setText("Delete"); + widgets.mDeleteButton.setEnabled(false); + widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + eventDisplay.removeDescriptor(descriptorClass, index); + fillUiWith(eventDisplay); + setModified(); + } + } + } + }); + + return widgets; + } + + + private void duplicateEventDisplay(ArrayList<EventDisplay> displayList) { + for (EventDisplay eventDisplay : displayList) { + mDisplayList.add(EventDisplay.clone(eventDisplay)); + } + } + + private void buildPidList(ArrayList<EventContainer> eventList) { + mPidList = new ArrayList<Integer>(); + for (EventContainer event : eventList) { + if (mPidList.indexOf(event.pid) == -1) { + mPidList.add(event.pid); + } + } + } + + private void setModified() { + mEditStatus = true; + } + + + private void enable(boolean status) { + mEventDisplayDeleteButton.setEnabled(status); + + // enable up/down + int selection = mEventDisplayList.getSelectionIndex(); + int count = mEventDisplayList.getItemCount(); + mEventDisplayUpButton.setEnabled(status && selection > 0); + mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1); + + mDisplayNameText.setEnabled(status); + mDisplayTypeCombo.setEnabled(status); + mPidFilterCheckBox.setEnabled(status); + + mValueSelection.setEnabled(status); + mOccurrenceSelection.setEnabled(status); + mValueSelection.mNewButton.setEnabled(status); + mOccurrenceSelection.mNewButton.setEnabled(status); + if (status == false) { + mPidText.setEnabled(false); + } + } + + private void fillEventDisplayList() { + for (EventDisplay eventDisplay : mDisplayList) { + mEventDisplayList.add(eventDisplay.getName()); + } + } + + private void createNewEventDisplay() { + int count = mDisplayList.size(); + + String name = String.format("display %1$d", count + 1); + + EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name); + + mDisplayList.add(eventDisplay); + mEventDisplayList.add(name); + + mEventDisplayList.select(count); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + + private void deleteEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.remove(selection); + mEventDisplayList.remove(selection); + if (mDisplayList.size() < selection) { + selection--; + } + mEventDisplayList.select(selection); + handleEventDisplaySelection(); + + setModified(); + } + } + + private EventDisplay getCurrentEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + return mDisplayList.get(selection); + } + + return null; + } + + private void setCurrentEventDisplay(EventDisplay eventDisplay) { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.set(selection, eventDisplay); + } + } + + private void handleEventDisplaySelection() { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // enable the UI + enable(true); + + // and fill it + fillUiWith(eventDisplay); + } else { + // disable the UI + enable(false); + + // and empty it. + emptyUi(); + } + } + + private void emptyUi() { + mDisplayNameText.setText(""); + mDisplayTypeCombo.clearSelection(); + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + } + + private void fillUiWith(EventDisplay eventDisplay) { + mProcessTextChanges = false; + + mDisplayNameText.setText(eventDisplay.getName()); + int displayMode = eventDisplay.getDisplayType(); + mDisplayTypeCombo.select(displayMode); + if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = false; + mChartOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getChartTimeLimit(); + if (limit != -1) { + mTimeLimitText.setText(Long.toString(limit)); + } else { + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = true; + mChartOptions.setVisible(!gd.exclude); + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + + if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = false; + mHistOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getHistWidth(); + if (limit != -1) { + mHistWidthText.setText(Long.toString(limit)); + } else { + mHistWidthText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = true; + mHistOptions.setVisible(!gd.exclude); + mHistWidthText.setText(""); //$NON-NLS-1$ + } + mInfoGroup.layout(true); + mShell.layout(true); + mShell.pack(); + + if (eventDisplay.getPidFiltering()) { + mPidFilterCheckBox.setSelection(true); + mPidText.setEnabled(true); + + // build the pid list. + ArrayList<Integer> list = eventDisplay.getPidFilterList(); + if (list != null) { + StringBuilder sb = new StringBuilder(); + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + sb.append(list.get(i)); + if (i < count - 1) { + sb.append(", ");//$NON-NLS-1$ + } + } + mPidText.setText(sb.toString()); + } else { + mPidText.setText(""); //$NON-NLS-1$ + } + } else { + mPidFilterCheckBox.setSelection(false); + mPidText.setEnabled(false); + mPidText.setText(""); //$NON-NLS-1$ + } + + mProcessTextChanges = true; + + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + + if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG || + eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) { + mOccurrenceSelection.setEnabled(true); + mValueSelection.setEnabled(true); + + Iterator<ValueDisplayDescriptor> valueIterator = eventDisplay.getValueDescriptors(); + + while (valueIterator.hasNext()) { + ValueDisplayDescriptor descriptor = valueIterator.next(); + mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s", + mEventTagMap.get(descriptor.eventTag), descriptor.valueName, + getSeriesLabelDescription(descriptor), getFilterDescription(descriptor))); + } + + Iterator<OccurrenceDisplayDescriptor> occurrenceIterator = + eventDisplay.getOccurrenceDescriptors(); + + while (occurrenceIterator.hasNext()) { + OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next(); + + mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s", + mEventTagMap.get(descriptor.eventTag), + getSeriesLabelDescription(descriptor), + getFilterDescription(descriptor))); + } + + mValueSelection.mList.notifyListeners(SWT.Selection, null); + mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null); + } else { + mOccurrenceSelection.setEnabled(false); + mValueSelection.setEnabled(false); + } + + } + + /** + * Returns a String describing what is used as the series label + * @param descriptor the descriptor of the display. + */ + private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid) { + return String.format("%1$s + pid", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.seriesValueIndex].getName()); + } else { + return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex] + .getName(); + } + } + return "pid"; + } + + private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.filterValueIndex != -1) { + return String.format(" [%1$s %2$s %3$s]", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.filterValueIndex].getName(), + descriptor.filterCompareMethod.testString(), + descriptor.filterValue != null ? + descriptor.filterValue.toString() : "?"); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java new file mode 100644 index 0000000..a1303f6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.Log; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +/** + * Imports a textual event log. Gets tags from build path. + */ +public class EventLogImporter { + + private String[] mTags; + private String[] mLog; + + public EventLogImporter(String filePath) throws FileNotFoundException { + String top = System.getenv("ANDROID_BUILD_TOP"); + if (top == null) { + throw new FileNotFoundException(); + } + final String tagFile = top + "/system/core/logcat/event-log-tags"; + BufferedReader tagReader = new BufferedReader( + new InputStreamReader(new FileInputStream(tagFile))); + BufferedReader eventReader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + try { + readTags(tagReader); + readLog(eventReader); + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + mTags = content.toArray(new String[content.size()]); + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java new file mode 100644 index 0000000..2621c6a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java @@ -0,0 +1,926 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.LogReceiver; +import com.android.ddmlib.log.LogReceiver.ILogListener; +import com.android.ddmlib.log.LogReceiver.LogEntry; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.TablePanel; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.annotation.UiThread; +import com.android.ddmuilib.annotation.WorkerThread; +import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * Event log viewer + */ +public class EventLogPanel extends TablePanel implements ILogListener, + ILogColumnListener { + + private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$ + + private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$ + private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$ + + static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$ + static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$ + + private final static int DEFAULT_DISPLAY_WIDTH = 500; + private final static int DEFAULT_DISPLAY_HEIGHT = 400; + + private IImageLoader mImageLoader; + + private Device mCurrentLoggedDevice; + private String mCurrentLogFile; + private LogReceiver mCurrentLogReceiver; + private EventLogParser mCurrentEventLogParser; + + private Object mLock = new Object(); + + /** list of all the events. */ + private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>(); + + /** list of all the new events, that have yet to be displayed by the ui */ + private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>(); + /** indicates a pending ui thread display */ + private boolean mPendingDisplay = false; + + /** list of all the custom event displays */ + private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>(); + + private final NumberFormat mFormatter = NumberFormat.getInstance(); + private Composite mParent; + private ScrolledComposite mBottomParentPanel; + private Composite mBottomPanel; + private ICommonAction mOptionsAction; + private ICommonAction mClearAction; + private ICommonAction mSaveAction; + private ICommonAction mLoadAction; + private ICommonAction mImportAction; + + /** file containing the current log raw data. */ + private File mTempFile = null; + + public EventLogPanel(IImageLoader imageLoader) { + super(); + mImageLoader = imageLoader; + mFormatter.setGroupingUsed(true); + } + + /** + * Sets the external actions. + * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code + * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. + * <p/>It will also make sure they are enabled only when possible. + * @param optionsAction + * @param clearAction + * @param saveAction + * @param loadAction + * @param importAction + */ + public void setActions(ICommonAction optionsAction, ICommonAction clearAction, + ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) { + mOptionsAction = optionsAction; + mOptionsAction.setRunnable(new Runnable() { + public void run() { + openOptionPanel(); + } + }); + + mClearAction = clearAction; + mClearAction.setRunnable(new Runnable() { + public void run() { + clearLog(); + } + }); + + mSaveAction = saveAction; + mSaveAction.setRunnable(new Runnable() { + public void run() { + try { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Save Event Log"); + fileDialog.setFileName("event.log"); + + String fileName = fileDialog.open(); + if (fileName != null) { + saveLog(fileName); + } + } catch (IOException e1) { + } + } + }); + + mLoadAction = loadAction; + mLoadAction.setRunnable(new Runnable() { + public void run() { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load Event Log"); + + String fileName = fileDialog.open(); + if (fileName != null) { + loadLog(fileName); + } + } + }); + + mImportAction = importAction; + mImportAction.setRunnable(new Runnable() { + public void run() { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Import Bug Report"); + + String fileName = fileDialog.open(); + if (fileName != null) { + importBugReport(fileName); + } + } + }); + + mOptionsAction.setEnabled(false); + mClearAction.setEnabled(false); + mSaveAction.setEnabled(false); + } + + /** + * Opens the option panel. + * </p> + * <b>This must be called from the UI thread</b> + */ + @UiThread + public void openOptionPanel() { + try { + EventDisplayOptions dialog = new EventDisplayOptions(mImageLoader, mParent.getShell()); + if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) { + synchronized (mLock) { + // get the new EventDisplay list + mEventDisplays.clear(); + mEventDisplays.addAll(dialog.getEventDisplays()); + + // since the list of EventDisplay changed, we store it. + saveEventDisplays(); + + rebuildUi(); + } + } + } catch (SWTException e) { + Log.e("EventLog", e); //$NON-NLS-1$ + } + } + + /** + * Clears the log. + * <p/> + * <b>This must be called from the UI thread</b> + */ + public void clearLog() { + try { + synchronized (mLock) { + mEvents.clear(); + mNewEvents.clear(); + mPendingDisplay = false; + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resetUI(); + } + } + } catch (SWTException e) { + Log.e("EventLog", e); //$NON-NLS-1$ + } + } + + /** + * Saves the content of the event log into a file. The log is saved in the same + * binary format than on the device. + * @param filePath + * @throws IOException + */ + public void saveLog(String filePath) throws IOException { + if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) { + File destFile = new File(filePath); + destFile.createNewFile(); + FileInputStream fis = new FileInputStream(mTempFile); + FileOutputStream fos = new FileOutputStream(destFile); + byte[] buffer = new byte[1024]; + + int count; + + while ((count = fis.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + + fos.close(); + fis.close(); + + // now we save the tag file + filePath = filePath + TAG_FILE_EXT; + mCurrentEventLogParser.saveTags(filePath); + } + } + + /** + * Loads a binary event log (if has associated .tag file) or + * otherwise loads a textual event log. + * @param filePath Event log path (and base of potential tag file) + */ + public void loadLog(String filePath) { + if ((new File(filePath + TAG_FILE_EXT)).exists()) { + startEventLogFromFiles(filePath); + } else { + try { + EventLogImporter importer = new EventLogImporter(filePath); + String[] tags = importer.getTags(); + String[] log = importer.getLog(); + startEventLogFromContent(tags, log); + } catch (FileNotFoundException e) { + // If this fails, display the error message from startEventLogFromFiles, + // and pretend we never tried EventLogImporter + Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog", + String.format("Failure to read %1$s", filePath + TAG_FILE_EXT)); + } + + } + } + + public void importBugReport(String filePath) { + try { + BugReportImporter importer = new BugReportImporter(filePath); + + String[] tags = importer.getTags(); + String[] log = importer.getLog(); + + startEventLogFromContent(tags, log); + + } catch (FileNotFoundException e) { + Log.logAndDisplay(LogLevel.ERROR, "Import", + "Unable to import bug report: " + e.getMessage()); + } + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected() + */ + @Override + public void clientSelected() { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected() + */ + @Override + public void deviceSelected() { + startEventLog(getCurrentDevice()); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int) + */ + public void clientChanged(Client client, int changeMask) { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite) + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + mParent.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + synchronized (mLock) { + if (mCurrentLogReceiver != null) { + mCurrentLogReceiver.cancel(); + mCurrentLogReceiver = null; + mCurrentEventLogParser = null; + mCurrentLoggedDevice = null; + mEventDisplays.clear(); + mEvents.clear(); + } + } + } + }); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // init some store stuff + store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH); + store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT); + + mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL); + mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + mBottomParentPanel.setExpandHorizontal(true); + mBottomParentPanel.setExpandVertical(true); + + mBottomParentPanel.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + if (mBottomPanel != null) { + Rectangle r = mBottomParentPanel.getClientArea(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, + SWT.DEFAULT)); + } + } + }); + + prepareDisplayUi(); + + // load the EventDisplay from storage. + loadEventDisplays(); + + // create the ui + createDisplayUi(); + + return mBottomParentPanel; + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#postCreation() + */ + @Override + protected void postCreation() { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#setFocus() + */ + @Override + public void setFocus() { + mBottomParentPanel.setFocus(); + } + + /** + * Starts a new logcat and set mCurrentLogCat as the current receiver. + * @param device the device to connect logcat to. + */ + private void startEventLog(final Device device) { + if (device == mCurrentLoggedDevice) { + return; + } + + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + if (device != null) { + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + while (device.isOnline() == false && + mCurrentLogReceiver != null && + mCurrentLogReceiver.isCancelled() == false) { + try { + sleep(2000); + } catch (InterruptedException e) { + return; + } + } + + if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) { + // logcat was stopped/cancelled before the device became ready. + return; + } + + try { + mCurrentLoggedDevice = device; + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + mCurrentEventLogParser.init(device); + } + + // update the event display with the new parser. + updateEventDisplays(); + + // prepare the temp file that will contain the raw data + mTempFile = File.createTempFile("android-event-", ".log"); + + device.runEventLogService(mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + } + + private void startEventLogFromFiles(final String fileName) { + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + mSaveAction.setEnabled(false); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + try { + mCurrentLogFile = fileName; + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) { + mCurrentEventLogParser = null; + Log.logAndDisplay(LogLevel.ERROR, "EventLog", + String.format("Failure to read %1$s", fileName + TAG_FILE_EXT)); + return; + } + } + + // update the event display with the new parser. + updateEventDisplays(); + + runLocalEventLogService(fileName, mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + + private void startEventLogFromContent(final String[] tags, final String[] log) { + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + mSaveAction.setEnabled(false); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + try { + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + if (mCurrentEventLogParser.init(tags) == false) { + mCurrentEventLogParser = null; + return; + } + } + + // update the event display with the new parser. + updateEventDisplays(); + + runLocalEventLogService(log, mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + + + public void stopEventLog(boolean inUiThread) { + if (mCurrentLogReceiver != null) { + mCurrentLogReceiver.cancel(); + + // when the thread finishes, no one will reference that object + // and it'll be destroyed + synchronized (mLock) { + mCurrentLogReceiver = null; + mCurrentEventLogParser = null; + + mCurrentLoggedDevice = null; + mEvents.clear(); + mNewEvents.clear(); + mPendingDisplay = false; + } + + resetUI(inUiThread); + } + + if (mTempFile != null) { + mTempFile.delete(); + mTempFile = null; + } + } + + private void resetUI(boolean inUiThread) { + mEvents.clear(); + + // the ui is static we just empty it. + if (inUiThread) { + resetUiFromUiThread(); + } else { + try { + Display d = mBottomParentPanel.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + resetUiFromUiThread(); + } + } + }); + } catch (SWTException e) { + // display is disposed, we're quitting. Do nothing. + } + } + } + + private void resetUiFromUiThread() { + synchronized(mLock) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resetUI(); + } + } + mOptionsAction.setEnabled(false); + mClearAction.setEnabled(false); + mSaveAction.setEnabled(false); + } + + private void prepareDisplayUi() { + mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE); + mBottomParentPanel.setContent(mBottomPanel); + } + + private void createDisplayUi() { + RowLayout rowLayout = new RowLayout(); + rowLayout.wrap = true; + rowLayout.pack = false; + rowLayout.justify = true; + rowLayout.fill = true; + rowLayout.type = SWT.HORIZONTAL; + mBottomPanel.setLayout(rowLayout); + + IPreferenceStore store = DdmUiPreferences.getStore(); + int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH); + int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT); + + for (EventDisplay eventDisplay : mEventDisplays) { + Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this); + if (c != null) { + RowData rd = new RowData(); + rd.height = displayHeight; + rd.width = displayWidth; + c.setLayoutData(rd); + } + + Table table = eventDisplay.getTable(); + if (table != null) { + addTableToFocusListener(table); + } + } + + mBottomPanel.layout(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + mBottomParentPanel.layout(); + } + + /** + * Rebuild the display ui. + */ + @UiThread + private void rebuildUi() { + synchronized (mLock) { + // we need to rebuild the ui. First we get rid of it. + mBottomPanel.dispose(); + mBottomPanel = null; + + prepareDisplayUi(); + createDisplayUi(); + + // and fill it + + boolean start_event = false; + synchronized (mNewEvents) { + mNewEvents.addAll(0, mEvents); + + if (mPendingDisplay == false) { + mPendingDisplay = true; + start_event = true; + } + } + + if (start_event) { + scheduleUIEventHandler(); + } + + Rectangle r = mBottomParentPanel.getClientArea(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, + SWT.DEFAULT)); + } + } + + + /** + * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it. + * @param entry The new log entry + * @see LogReceiver.ILogListener#newEntry(LogEntry) + */ + @WorkerThread + public void newEntry(LogEntry entry) { + synchronized (mLock) { + if (mCurrentEventLogParser != null) { + EventContainer event = mCurrentEventLogParser.parse(entry); + if (event != null) { + handleNewEvent(event); + } + } + } + } + + @WorkerThread + private void handleNewEvent(EventContainer event) { + // add the event to the generic list + mEvents.add(event); + + // add to the list of events that needs to be displayed, and trigger a + // new display if needed. + boolean start_event = false; + synchronized (mNewEvents) { + mNewEvents.add(event); + + if (mPendingDisplay == false) { + mPendingDisplay = true; + start_event = true; + } + } + + if (start_event == false) { + // we're done + return; + } + + scheduleUIEventHandler(); + } + + /** + * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}. + */ + private void scheduleUIEventHandler() { + try { + Display d = mBottomParentPanel.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + if (mCurrentEventLogParser != null) { + displayNewEvents(); + } + } + } + }); + } catch (SWTException e) { + // if the ui is disposed, do nothing + } + } + + /** + * Processes raw data coming from the log service. + * @see LogReceiver.ILogListener#newData(byte[], int, int) + */ + public void newData(byte[] data, int offset, int length) { + if (mTempFile != null) { + try { + FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */); + fos.write(data, offset, length); + fos.close(); + } catch (FileNotFoundException e) { + } catch (IOException e) { + } + } + } + + @UiThread + private void displayNewEvents() { + // never display more than 1,000 events in this loop. We can't do too much in the UI thread. + int count = 0; + + // prepare the displays + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.startMultiEventDisplay(); + } + + // display the new events + EventContainer event = null; + boolean need_to_reloop = false; + do { + // get the next event to display. + synchronized (mNewEvents) { + if (mNewEvents.size() > 0) { + if (count > 200) { + // there are still events to be displayed, but we don't want to hog the + // UI thread for too long, so we stop this runnable, but launch a new + // one to keep going. + need_to_reloop = true; + event = null; + } else { + event = mNewEvents.remove(0); + count++; + } + } else { + // we're done. + event = null; + mPendingDisplay = false; + } + } + + if (event != null) { + // notify the event display + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.newEvent(event, mCurrentEventLogParser); + } + } + } while (event != null); + + // we're done displaying events. + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.endMultiEventDisplay(); + } + + // if needed, ask the UI thread to re-run this method. + if (need_to_reloop) { + scheduleUIEventHandler(); + } + } + + /** + * Loads the {@link EventDisplay}s from the preference store. + */ + private void loadEventDisplays() { + IPreferenceStore store = DdmUiPreferences.getStore(); + String storage = store.getString(PREFS_EVENT_DISPLAY); + + if (storage.length() > 0) { + String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR)); + + for (String value : values) { + EventDisplay eventDisplay = EventDisplay.load(value); + if (eventDisplay != null) { + mEventDisplays.add(eventDisplay); + } + } + } + } + + /** + * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store. + */ + private void saveEventDisplays() { + IPreferenceStore store = DdmUiPreferences.getStore(); + + boolean first = true; + StringBuilder sb = new StringBuilder(); + + for (EventDisplay eventDisplay : mEventDisplays) { + String storage = eventDisplay.getStorageString(); + if (storage != null) { + if (first == false) { + sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR); + } else { + first = false; + } + + sb.append(storage); + } + } + + store.setValue(PREFS_EVENT_DISPLAY, sb.toString()); + } + + /** + * Updates the {@link EventDisplay} with the new {@link EventLogParser}. + * <p/> + * This will run asynchronously in the UI thread. + */ + @WorkerThread + private void updateEventDisplays() { + try { + Display d = mBottomParentPanel.getDisplay(); + + d.asyncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.setNewLogParser(mCurrentEventLogParser); + } + + mOptionsAction.setEnabled(true); + mClearAction.setEnabled(true); + if (mCurrentLogFile == null) { + mSaveAction.setEnabled(true); + } else { + mSaveAction.setEnabled(false); + } + } + } + }); + } catch (SWTException e) { + // display is disposed: do nothing. + } + } + + @UiThread + public void columnResized(int index, TableColumn sourceColumn) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resizeColumn(index, sourceColumn); + } + } + + /** + * Runs an event log service out of a local file. + * @param fileName the full file name of the local file containing the event log. + * @param logReceiver the receiver that will handle the log + * @throws IOException + */ + @WorkerThread + private void runLocalEventLogService(String fileName, LogReceiver logReceiver) + throws IOException { + byte[] buffer = new byte[256]; + + FileInputStream fis = new FileInputStream(fileName); + + int count; + while ((count = fis.read(buffer)) != -1) { + logReceiver.parseNewData(buffer, 0, count); + } + } + + @WorkerThread + private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) { + synchronized (mLock) { + for (String line : log) { + EventContainer event = mCurrentEventLogParser.parse(line); + if (event != null) { + handleNewEvent(event); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 0000000..dd32e2c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +final class EventValueSelector extends Dialog { + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 300; + + private Shell mParent; + private Shell mShell; + private boolean mEditStatus; + private Combo mEventCombo; + private Combo mValueCombo; + private Combo mSeriesCombo; + private Button mDisplayPidCheckBox; + private Combo mFilterCombo; + private Combo mFilterMethodCombo; + private Text mFilterValue; + private Button mOkButton; + + private EventLogParser mLogParser; + private OccurrenceDisplayDescriptor mDescriptor; + + /** list of event integer in the order of the combo. */ + private Integer[] mEventTags; + + /** list of indices in the {@link EventValueDescription} array of the current event + * that are of type string. This lets us get back the {@link EventValueDescription} from the + * index in the Series {@link Combo}. + */ + private final ArrayList<Integer> mSeriesIndices = new ArrayList<Integer>(); + + public EventValueSelector(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Opens the display option dialog to edit a new descriptor. + * @param decriptorClass the class of the object to instantiate. Must extend + * {@link OccurrenceDisplayDescriptor} + * @param logParser + * @return true if the object is to be created, false if the creation was canceled. + */ + boolean open(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, + EventLogParser logParser) { + try { + OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance(); + setModified(); + return open(descriptor, logParser); + } catch (InstantiationException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } + } + + /** + * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or + * a {@link ValueDisplayDescriptor} object. + * @param descriptor The descriptor to edit. + * @return true if the object was modified. + */ + boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) { + // make a copy of the descriptor as we'll use a working copy. + if (descriptor instanceof ValueDisplayDescriptor) { + mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor); + } else if (descriptor instanceof OccurrenceDisplayDescriptor) { + mDescriptor = new OccurrenceDisplayDescriptor(descriptor); + } else { + return false; + } + + mLogParser = logParser; + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + loadValueDescriptor(); + + checkValidity(); + + // Set the dialog size. + try { + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + } catch (Exception e) { + e.printStackTrace(); + } + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + OccurrenceDisplayDescriptor getDescriptor() { + return mDescriptor; + } + + private void createUI() { + GridData gd; + + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(2, false)); + + Label l = new Label(mShell, SWT.NONE); + l.setText("Event:"); + + mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // the event tag / event name map + Map<Integer, String> eventTagMap = mLogParser.getTagMap(); + Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap(); + Set<Integer> keys = eventTagMap.keySet(); + ArrayList<Integer> list = new ArrayList<Integer>(); + for (Integer i : keys) { + if (eventInfoMap.get(i) != null) { + String eventName = eventTagMap.get(i); + mEventCombo.add(eventName); + + list.add(i); + } + } + mEventTags = list.toArray(new Integer[list.size()]); + + mEventCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleEventComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Value:"); + + mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mValueCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleValueComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Series Name:"); + + mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSeriesCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleSeriesComboSelection(); + setModified(); + } + }); + + // empty comp + new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mDisplayPidCheckBox = new Button(mShell, SWT.CHECK); + mDisplayPidCheckBox.setText("Also Show pid"); + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mDescriptor.includePid = mDisplayPidCheckBox.getSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter By:"); + + mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Method:"); + + mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (CompareMethod method : CompareMethod.values()) { + mFilterMethodCombo.add(method.toString()); + } + mFilterMethodCombo.select(0); + mFilterMethodCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterMethodComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Value:"); + + mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE); + mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterValue.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mDescriptor.filterValueIndex != -1) { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription valueDesc = mLogParser.getEventInfoMap() + .get(eventTag)[mDescriptor.filterValueIndex]; + + // let the EventValueDescription convert the String value into an object + // of the proper type. + mDescriptor.filterValue = valueDesc.getObjectFromString( + mFilterValue.getText().trim()); + setModified(); + } + } + } + }); + + // add a separator spanning the 2 columns + + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + + // add a composite to hold the ok/cancel button, no matter what the columns size are. + Composite buttonComp = new Composite(mShell, SWT.NONE); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + buttonComp.setLayoutData(gd); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(6, true)); + gl.marginHeight = gl.marginWidth = 0; + + Composite padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mOkButton = new Button(buttonComp, SWT.PUSH); + mOkButton.setText("OK"); + mOkButton.setLayoutData(new GridData(GridData.CENTER)); + mOkButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Button cancelButton = new Button(buttonComp, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(GridData.CENTER)); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the edit + mEditStatus = false; + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + } + + private void setModified() { + mEditStatus = true; + } + + private void handleEventComboSelection() { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + // fill the combo for the values + mValueCombo.removeAll(); + if (values != null) { + if (mDescriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + mValueCombo.setEnabled(true); + for (EventValueDescription value : values) { + mValueCombo.add(value.toString()); + } + + if (valueDescriptor.valueIndex != -1) { + mValueCombo.select(valueDescriptor.valueIndex); + } else { + mValueCombo.clearSelection(); + } + } else { + mValueCombo.setEnabled(false); + } + + // fill the axis combo + mSeriesCombo.removeAll(); + mSeriesCombo.setEnabled(false); + mSeriesIndices.clear(); + int axisIndex = 0; + int selectionIndex = -1; + for (EventValueDescription value : values) { + if (value.getEventValueType() == EventValueType.STRING) { + mSeriesCombo.add(value.getName()); + mSeriesCombo.setEnabled(true); + mSeriesIndices.add(axisIndex); + + if (mDescriptor.seriesValueIndex != -1 && + mDescriptor.seriesValueIndex == axisIndex) { + selectionIndex = axisIndex; + } + } + axisIndex++; + } + + if (mSeriesCombo.isEnabled()) { + mSeriesCombo.add("default (pid)", 0 /* index */); + mSeriesIndices.add(0 /* index */, -1 /* value */); + + // +1 because we added another item at index 0 + mSeriesCombo.select(selectionIndex + 1); + + if (selectionIndex >= 0) { + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + mDisplayPidCheckBox.setEnabled(true); + } else { + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + } + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + + // fill the filter combo + mFilterCombo.setEnabled(true); + mFilterCombo.removeAll(); + mFilterCombo.add("(no filter)"); + for (EventValueDescription value : values) { + mFilterCombo.add(value.toString()); + } + + // select the current filter + mFilterCombo.select(mDescriptor.filterValueIndex + 1); + mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod)); + + // fill the current filter value + if (mDescriptor.filterValueIndex != -1) { + EventValueDescription valueInfo = values[mDescriptor.filterValueIndex]; + if (valueInfo.checkForType(mDescriptor.filterValue)) { + mFilterValue.setText(mDescriptor.filterValue.toString()); + } else { + mFilterValue.setText(""); + } + } else { + mFilterValue.setText(""); + } + } else { + disableSubCombos(); + } + } else { + disableSubCombos(); + } + + checkValidity(); + } + + /** + * + */ + private void disableSubCombos() { + mValueCombo.removeAll(); + mValueCombo.clearSelection(); + mValueCombo.setEnabled(false); + + mSeriesCombo.removeAll(); + mSeriesCombo.clearSelection(); + mSeriesCombo.setEnabled(false); + + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + + mFilterCombo.removeAll(); + mFilterCombo.clearSelection(); + mFilterCombo.setEnabled(false); + + mFilterValue.setEnabled(false); + mFilterValue.setText(""); + mFilterMethodCombo.setEnabled(false); + } + + private void handleValueComboSelection() { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + // get the current selection in the value combo + int index = mValueCombo.getSelectionIndex(); + valueDescriptor.valueIndex = index; + + // for now set the built-in name + + // get the current selection in the event combo + int eventIndex = mEventCombo.getSelectionIndex(); + + // match it to an event + int eventTag = mEventTags[eventIndex]; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + valueDescriptor.valueName = values[index].getName(); + + checkValidity(); + } + + private void handleSeriesComboSelection() { + // get the current selection in the axis combo + int index = mSeriesCombo.getSelectionIndex(); + + // get the actual value index from the list. + int valueIndex = mSeriesIndices.get(index); + + mDescriptor.seriesValueIndex = valueIndex; + + if (index > 0) { + mDisplayPidCheckBox.setEnabled(true); + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + } + + private void handleFilterComboSelection() { + // get the current selection in the axis combo + int index = mFilterCombo.getSelectionIndex(); + + // decrement index by 1 since the item 0 means + // no filter (index = -1), and the rest is offset by 1 + index--; + + mDescriptor.filterValueIndex = index; + + if (index != -1) { + mFilterValue.setEnabled(true); + mFilterMethodCombo.setEnabled(true); + if (mDescriptor.filterValue instanceof String) { + mFilterValue.setText((String)mDescriptor.filterValue); + } + } else { + mFilterValue.setText(""); + mFilterValue.setEnabled(false); + mFilterMethodCombo.setEnabled(false); + } + } + + private void handleFilterMethodComboSelection() { + // get the current selection in the axis combo + int index = mFilterMethodCombo.getSelectionIndex(); + CompareMethod method = CompareMethod.values()[index]; + + mDescriptor.filterCompareMethod = method; + } + + /** + * Returns the index of the filter method + * @param filterCompareMethod the {@link CompareMethod} enum. + */ + private int getFilterMethodIndex(CompareMethod filterCompareMethod) { + CompareMethod[] values = CompareMethod.values(); + for (int i = 0 ; i < values.length ; i++) { + if (values[i] == filterCompareMethod) { + return i; + } + } + return -1; + } + + + private void loadValueDescriptor() { + // get the index from the eventTag. + int eventIndex = 0; + int comboIndex = -1; + for (int i : mEventTags) { + if (i == mDescriptor.eventTag) { + comboIndex = eventIndex; + break; + } + eventIndex++; + } + + if (comboIndex == -1) { + mEventCombo.clearSelection(); + } else { + mEventCombo.select(comboIndex); + } + + // get the event from the descriptor + handleEventComboSelection(); + } + + private void checkValidity() { + mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 && + (((mDescriptor instanceof ValueDisplayDescriptor) == false) || + mValueCombo.getSelectionIndex() != -1)); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 0000000..3af1447 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.CrosshairState; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYItemRendererState; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.data.xy.XYDataset; +import org.jfree.ui.RectangleEdge; + +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +/** + * Custom renderer to render event occurrence. This rendered ignores the y value, and simply + * draws a line from min to max at the time of the item. + */ +public class OccurrenceRenderer extends XYLineAndShapeRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public void drawItem(Graphics2D g2, + XYItemRendererState state, + Rectangle2D dataArea, + PlotRenderingInfo info, + XYPlot plot, + ValueAxis domainAxis, + ValueAxis rangeAxis, + XYDataset dataset, + int series, + int item, + CrosshairState crosshairState, + int pass) { + TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset; + + // get the x value for the series/item. + double x = timeDataSet.getX(series, item).doubleValue(); + + // get the min/max of the range axis + double yMin = rangeAxis.getLowerBound(); + double yMax = rangeAxis.getUpperBound(); + + RectangleEdge domainEdge = plot.getDomainAxisEdge(); + RectangleEdge rangeEdge = plot.getRangeAxisEdge(); + + // convert the coordinates to java2d. + double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge); + double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge); + double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge); + + // get the paint information for the series/item + Paint p = getItemPaint(series, item); + Stroke s = getItemStroke(series, item); + + Line2D line = null; + PlotOrientation orientation = plot.getOrientation(); + if (orientation == PlotOrientation.HORIZONTAL) { + line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D); + } + else if (orientation == PlotOrientation.VERTICAL) { + line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D); + } + g2.setPaint(p); + g2.setStroke(s); + g2.draw(line); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 0000000..108c097 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; + +import java.awt.Color; + +abstract public class SyncCommon extends EventDisplay { + + // State information while processing the event stream + private int mLastState; // 0 if event started, 1 if event stopped + private long mLastStartTime; // ms + private long mLastStopTime; //ms + private String mLastDetails; + private int mLastSyncSource; // poll, server, user, etc. + + // Some common variables for sync display. These define the sync backends + //and how they should be displayed. + protected static final int CALENDAR = 0; + protected static final int GMAIL = 1; + protected static final int FEEDS = 2; + protected static final int CONTACTS = 3; + protected static final int ERRORS = 4; + protected static final int NUM_AUTHS = (CONTACTS + 1); + protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "Errors"}; + protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED}; + + // Values from data/etc/event-log-tags + final int EVENT_SYNC = 2720; + final int EVENT_TICKLE = 2742; + final int EVENT_SYNC_DETAILS = 2743; + + protected SyncCommon(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLastStartTime = 0; + mLastStopTime = 0; + mLastState = -1; + mLastSyncSource = -1; + mLastDetails = ""; + } + + /** + * Updates the display with a new event. This is the main entry point for + * each event. This method has the logic to tie together the start event, + * stop event, and details event into one graph item. The combined sync event + * is handed to the subclass via processSycnEvent. Note that the details + * can happen before or after the stop event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + try { + if (event.mTag == EVENT_SYNC) { + int state = Integer.parseInt(event.getValueAsString(1)); + if (state == 0) { // start + mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + mLastState = 0; + mLastSyncSource = Integer.parseInt(event.getValueAsString(2)); + mLastDetails = ""; + } else if (state == 1) { // stop + if (mLastState == 0) { + mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (mLastStartTime == 0) { + // Log starts with a stop event + mLastStartTime = mLastStopTime; + } + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + true, mLastSyncSource); + mLastState = 1; + } + } + } else if (event.mTag == EVENT_SYNC_DETAILS) { + mLastDetails = event.getValueAsString(3); + if (mLastState != 0) { // Not inside event + long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (updateTime - mLastStopTime <= 250) { + // Got details within 250ms after event, so delete and re-insert + // Details later than 250ms (arbitrary) are discarded as probably + // unrelated. + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + false, mLastSyncSource); + } + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback hook for subclass to process a sync event. newEvent has the logic + * to combine start and stop events and passes a processed event to the + * subclass. + * + * @param event The sync event + * @param auth The sync authority + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource Poll, user, server, etc. + */ + abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource); + + /** + * Converts authority name to auth number. + * + * @param authname "calendar", etc. + * @return number series number associated with the authority + */ + protected int getAuth(String authname) throws InvalidTypeException { + if ("calendar".equals(authname) || "cl".equals(authname)) { + return CALENDAR; + } else if ("contacts".equals(authname) || "cp".equals(authname)) { + return CONTACTS; + } else if ("subscribedfeeds".equals(authname)) { + return FEEDS; + } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { + return GMAIL; + } else if ("gmail-live".equals(authname)) { + return GMAIL; + } else if ("unknown".equals(authname)) { + return -1; // Unknown tickles; discard + } else { + throw new InvalidTypeException("Unknown authname " + authname); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 0000000..c66fe48 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import com.android.ddmuilib.IImageLoader; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Small dialog box to edit a static port number. + */ +public class EditFilterDialog extends Dialog { + + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 250; + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + private IImageLoader mImageLoader; + + /** + * Filter being edited or created + */ + private LogFilter mFilter; + + private String mName; + private String mTag; + private String mPid; + + /** Log level as an index of the drop-down combo + * @see getLogLevel + * @see getComboIndex + */ + private int mLogLevel; + + private Button mOkButton; + + private Label mPidWarning; + + public EditFilterDialog(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + public EditFilterDialog(IImageLoader imageLoader, Shell shell, + LogFilter filter) { + this(imageLoader, shell); + mFilter = filter; + } + + /** + * Opens the dialog. The method will return when the user closes the dialog + * somehow. + * + * @return true if ok was pressed, false if cancelled. + */ + public boolean open() { + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.open(); + + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + // we're quitting with OK. + // Lets update the filter if needed + if (mOk) { + // if it was a "Create filter" action we need to create it first. + if (mFilter == null) { + mFilter = new LogFilter(mName); + } + + // setup the filter + mFilter.setTagMode(mTag); + + if (mPid != null && mPid.length() > 0) { + mFilter.setPidMode(Integer.parseInt(mPid)); + } else { + mFilter.setPidMode(-1); + } + + mFilter.setLogLevel(getLogLevel(mLogLevel)); + } + + return mOk; + } + + public LogFilter getFilter() { + return mFilter; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Log Filter"); + + mShell.setLayout(new GridLayout(1, false)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + } + }); + + // top part with the filter name + Composite nameComposite = new Composite(mShell, SWT.NONE); + nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + nameComposite.setLayout(new GridLayout(2, false)); + + Label l = new Label(nameComposite, SWT.NONE); + l.setText("Filter Name:"); + + final Text filterNameText = new Text(nameComposite, + SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mName = mFilter.getName(); + if (mName != null) { + filterNameText.setText(mName); + } + } + filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + filterNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mName = filterNameText.getText().trim(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + + // center part with the filter parameters + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(3, false)); + + l = new Label(main, SWT.NONE); + l.setText("by Log Tag:"); + + final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mTag = mFilter.getTagFilter(); + if (mTag != null) { + tagText.setText(mTag); + } + } + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + tagText.setLayoutData(gd); + tagText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mTag = tagText.getText().trim(); + validate(); + } + }); + + l = new Label(main, SWT.NONE); + l.setText("by pid:"); + + final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + if (mFilter.getPidFilter() != -1) { + mPid = Integer.toString(mFilter.getPidFilter()); + } else { + mPid = ""; + } + pidText.setText(mPid); + } + pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + pidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mPid = pidText.getText().trim(); + validate(); + } + }); + + mPidWarning = new Label(main, SWT.NONE); + mPidWarning.setImage(mImageLoader.loadImage("empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + + l = new Label(main, SWT.NONE); + l.setText("by Log level:"); + + final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + logCombo.setLayoutData(gd); + + // add the labels + logCombo.add("<none>"); + logCombo.add("Error"); + logCombo.add("Warning"); + logCombo.add("Info"); + logCombo.add("Debug"); + logCombo.add("Verbose"); + + if (mFilter != null) { + mLogLevel = getComboIndex(mFilter.getLogLevel()); + logCombo.select(mLogLevel); + } else { + logCombo.select(0); + } + + logCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection + mLogLevel = logCombo.getSelectionIndex(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Returns the log level from a combo index. + * @param index the Combo index + * @return a log level valid for the Log class. + */ + protected int getLogLevel(int index) { + if (index == 0) { + return -1; + } + + return 7 - index; + } + + /** + * Returns the index in the combo that matches the log level + * @param logLevel The Log level. + * @return the combo index + */ + private int getComboIndex(int logLevel) { + if (logLevel == -1) { + return 0; + } + + return 7 - logLevel; + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + + // then we check it only contains digits. + if (mPid != null) { + if (mPid.matches("[0-9]*") == false) { // $NON-NLS-1$ + mOkButton.setEnabled(false); + mPidWarning.setImage(mImageLoader.loadImage( + "warning.png", // $NON-NLS-1$ + mShell.getDisplay())); + return; + } else { + mPidWarning.setImage(mImageLoader.loadImage( + "empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + } + } + + if (mName == null || mName.length() == 0) { + mOkButton.setEnabled(false); + return; + } + + mOkButton.setEnabled(true); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 0000000..9cff656 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import org.eclipse.swt.graphics.Color; + +public class LogColors { + public Color infoColor; + public Color debugColor; + public Color errorColor; + public Color warningColor; + public Color verboseColor; +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 0000000..a32de2f --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.annotation.UiThread; +import com.android.ddmuilib.logcat.LogPanel.LogMessage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.regex.PatternSyntaxException; + +/** logcat output filter class */ +public class LogFilter { + + public final static int MODE_PID = 0x01; + public final static int MODE_TAG = 0x02; + public final static int MODE_LEVEL = 0x04; + + private String mName; + + /** + * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL + */ + private int mMode = 0; + + /** + * pid used for filtering. Only valid if mMode is MODE_PID. + */ + private int mPid; + + /** Single level log level as defined in Log.mLevelChar. Only valid + * if mMode is MODE_LEVEL */ + private int mLogLevel; + + /** + * log tag filtering. Only valid if mMode is MODE_TAG + */ + private String mTag; + + private Table mTable; + private TabItem mTabItem; + private boolean mIsCurrentTabItem = false; + private int mUnreadCount = 0; + + /** Temp keyword filtering */ + private String[] mTempKeywordFilters; + + /** temp pid filtering */ + private int mTempPid = -1; + + /** temp tag filtering */ + private String mTempTag; + + /** temp log level filtering */ + private int mTempLogLevel = -1; + + private LogColors mColors; + + private boolean mTempFilteringStatus = false; + + private final ArrayList<LogMessage> mMessages = new ArrayList<LogMessage>(); + private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>(); + + private boolean mSupportsDelete = true; + private boolean mSupportsEdit = true; + private int mRemovedMessageCount = 0; + + /** + * Creates a filter with a particular mode. + * @param name The name to be displayed in the UI + */ + public LogFilter(String name) { + mName = name; + } + + public LogFilter() { + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mName); + + sb.append(':'); + sb.append(mMode); + if ((mMode & MODE_PID) == MODE_PID) { + sb.append(':'); + sb.append(mPid); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + sb.append(':'); + sb.append(mLogLevel); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + sb.append(':'); + sb.append(mTag); + } + + return sb.toString(); + } + + public boolean loadFromString(String string) { + String[] segments = string.split(":"); // $NON-NLS-1$ + int index = 0; + + // get the name + mName = segments[index++]; + + // get the mode + mMode = Integer.parseInt(segments[index++]); + + if ((mMode & MODE_PID) == MODE_PID) { + mPid = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + mLogLevel = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + mTag = segments[index++]; + } + + return true; + } + + + /** Sets the name of the filter. */ + void setName(String name) { + mName = name; + } + + /** + * Returns the UI display name. + */ + public String getName() { + return mName; + } + + /** + * Set the Table ui widget associated with this filter. + * @param tabItem The item in the TabFolder + * @param table The Table object + */ + public void setWidgets(TabItem tabItem, Table table) { + mTable = table; + mTabItem = tabItem; + } + + /** + * Returns true if the filter is ready for ui. + */ + public boolean uiReady() { + return (mTable != null && mTabItem != null); + } + + /** + * Returns the UI table object. + * @return + */ + public Table getTable() { + return mTable; + } + + public void dispose() { + mTable.dispose(); + mTabItem.dispose(); + mTable = null; + mTabItem = null; + } + + /** + * Resets the filtering mode to be 0 (i.e. no filter). + */ + public void resetFilteringMode() { + mMode = 0; + } + + /** + * Returns the current filtering mode. + * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL + */ + public int getFilteringMode() { + return mMode; + } + + /** + * Adds PID to the current filtering mode. + * @param pid + */ + public void setPidMode(int pid) { + if (pid != -1) { + mMode |= MODE_PID; + } else { + mMode &= ~MODE_PID; + } + mPid = pid; + } + + /** Returns the pid filter if valid, otherwise -1 */ + public int getPidFilter() { + if ((mMode & MODE_PID) == MODE_PID) + return mPid; + return -1; + } + + public void setTagMode(String tag) { + if (tag != null && tag.length() > 0) { + mMode |= MODE_TAG; + } else { + mMode &= ~MODE_TAG; + } + mTag = tag; + } + + public String getTagFilter() { + if ((mMode & MODE_TAG) == MODE_TAG) + return mTag; + return null; + } + + public void setLogLevel(int level) { + if (level == -1) { + mMode &= ~MODE_LEVEL; + } else { + mMode |= MODE_LEVEL; + mLogLevel = level; + } + + } + + public int getLogLevel() { + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + return mLogLevel; + } + + return -1; + } + + + public boolean supportsDelete() { + return mSupportsDelete ; + } + + public boolean supportsEdit() { + return mSupportsEdit; + } + + /** + * Sets the selected state of the filter. + * @param selected selection state. + */ + public void setSelectedState(boolean selected) { + if (selected) { + if (mTabItem != null) { + mTabItem.setText(mName); + } + mUnreadCount = 0; + } + mIsCurrentTabItem = selected; + } + + /** + * Adds a new message and optionally removes an old message. + * <p/>The new message is filtered through {@link #accept(LogMessage)}. + * Calls to {@link #flush()} from a UI thread will display it (and other + * pending messages) to the associated {@link Table}. + * @param logMessage the MessageData object to filter + * @return true if the message was accepted. + */ + public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) { + synchronized (mMessages) { + if (oldMessage != null) { + int index = mMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mMessages.remove(index); + mRemovedMessageCount++; + } + + // now we look for it in mNewMessages. This can happen if the new message is added + // and then removed because too many messages are added between calls to #flush() + index = mNewMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mNewMessages.remove(index); + } + } + + boolean filter = accept(newMessage); + + if (filter) { + // at this point the message is accepted, we add it to the list + mMessages.add(newMessage); + mNewMessages.add(newMessage); + } + + return filter; + } + } + + /** + * Removes all the items in the filter and its {@link Table}. + */ + public void clear() { + mRemovedMessageCount = 0; + mNewMessages.clear(); + mMessages.clear(); + mTable.removeAll(); + } + + /** + * Filters a message. + * @param logMessage the Message + * @return true if the message is accepted by the filter. + */ + boolean accept(LogMessage logMessage) { + // do the regular filtering now + if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) { + return false; + } + + if ((mMode & MODE_TAG) == MODE_TAG && ( + logMessage.data.tag == null || + logMessage.data.tag.equals(mTag) == false)) { + return false; + } + + int msgLogLevel = logMessage.data.logLevel.getPriority(); + + // test the temp log filtering first, as it replaces the old one + if (mTempLogLevel != -1) { + if (mTempLogLevel > msgLogLevel) { + return false; + } + } else if ((mMode & MODE_LEVEL) == MODE_LEVEL && + mLogLevel > msgLogLevel) { + return false; + } + + // do the temp filtering now. + if (mTempKeywordFilters != null) { + String msg = logMessage.msg; + + for (String kw : mTempKeywordFilters) { + try { + if (msg.contains(kw) == false && msg.matches(kw) == false) { + return false; + } + } catch (PatternSyntaxException e) { + // if the string is not a valid regular expression, + // this exception is thrown. + return false; + } + } + } + + if (mTempPid != -1 && mTempPid != logMessage.data.pid) { + return false; + } + + if (mTempTag != null && mTempTag.length() > 0) { + if (mTempTag.equals(logMessage.data.tag) == false) { + return false; + } + } + + return true; + } + + /** + * Takes all the accepted messages and display them. + * This must be called from a UI thread. + */ + @UiThread + public void flush() { + // if scroll bar is at the bottom, we will scroll + ScrollBar bar = mTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // if we are not going to scroll, get the current first item being shown. + int topIndex = mTable.getTopIndex(); + + // disable drawing + mTable.setRedraw(false); + + int totalCount = mNewMessages.size(); + + try { + // remove the items of the old messages. + for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) { + mTable.remove(0); + } + + if (mUnreadCount > mTable.getItemCount()) { + mUnreadCount = mTable.getItemCount(); + } + + // add the new items + for (int i = 0 ; i < totalCount ; i++) { + LogMessage msg = mNewMessages.get(i); + addTableItem(msg); + } + } catch (SWTException e) { + // log the error and keep going. Content of the logcat table maybe unexpected + // but at least ddms won't crash. + Log.e("LogFilter", e); + } + + // redraw + mTable.setRedraw(true); + + // scroll if needed, by showing the last item + if (scroll) { + totalCount = mTable.getItemCount(); + if (totalCount > 0) { + mTable.showItem(mTable.getItem(totalCount-1)); + } + } else if (mRemovedMessageCount > 0) { + // we need to make sure the topIndex is still visible. + // Because really old items are removed from the list, this could make it disappear + // if we don't change the scroll value at all. + + topIndex -= mRemovedMessageCount; + if (topIndex < 0) { + // looks like it disappeared. Lets just show the first item + mTable.showItem(mTable.getItem(0)); + } else { + mTable.showItem(mTable.getItem(topIndex)); + } + } + + // if this filter is not the current one, we update the tab text + // with the amount of unread message + if (mIsCurrentTabItem == false) { + mUnreadCount += mNewMessages.size(); + totalCount = mTable.getItemCount(); + if (mUnreadCount > 0) { + mTabItem.setText(mName + " (" // $NON-NLS-1$ + + (mUnreadCount > totalCount ? totalCount : mUnreadCount) + + ")"); // $NON-NLS-1$ + } else { + mTabItem.setText(mName); // $NON-NLS-1$ + } + } + + mNewMessages.clear(); + } + + void setColors(LogColors colors) { + mColors = colors; + } + + int getUnreadCount() { + return mUnreadCount; + } + + void setUnreadCount(int unreadCount) { + mUnreadCount = unreadCount; + } + + void setSupportsDelete(boolean support) { + mSupportsDelete = support; + } + + void setSupportsEdit(boolean support) { + mSupportsEdit = support; + } + + void setTempKeywordFiltering(String[] segments) { + mTempKeywordFilters = segments; + mTempFilteringStatus = true; + } + + void setTempPidFiltering(int pid) { + mTempPid = pid; + mTempFilteringStatus = true; + } + + void setTempTagFiltering(String tag) { + mTempTag = tag; + mTempFilteringStatus = true; + } + + void resetTempFiltering() { + if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) { + mTempFilteringStatus = true; + } + + mTempPid = -1; + mTempTag = null; + mTempKeywordFilters = null; + } + + void resetTempFilteringStatus() { + mTempFilteringStatus = false; + } + + boolean getTempFilterStatus() { + return mTempFilteringStatus; + } + + + /** + * Add a TableItem for the index-th item of the buffer + * @param filter The index of the table in which to insert the item. + */ + private void addTableItem(LogMessage msg) { + TableItem item = new TableItem(mTable, SWT.NONE); + item.setText(0, msg.data.time); + item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() })); + item.setText(2, msg.data.pidString); + item.setText(3, msg.data.tag); + item.setText(4, msg.msg); + + // add the buffer index as data + item.setData(msg); + + if (msg.data.logLevel == LogLevel.INFO) { + item.setForeground(mColors.infoColor); + } else if (msg.data.logLevel == LogLevel.DEBUG) { + item.setForeground(mColors.debugColor); + } else if (msg.data.logLevel == LogLevel.ERROR) { + item.setForeground(mColors.errorColor); + } else if (msg.data.logLevel == LogLevel.WARN) { + item.setForeground(mColors.warningColor); + } else { + item.setForeground(mColors.verboseColor); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java new file mode 100644 index 0000000..bd8b75c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java @@ -0,0 +1,1571 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.actions.ICommonAction; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LogPanel extends SelectionDependentPanel { + + private static final int STRING_BUFFER_LENGTH = 10000; + + /** no filtering. Only one tab with everything. */ + public static final int FILTER_NONE = 0; + /** manual mode for filter. all filters are manually created. */ + public static final int FILTER_MANUAL = 1; + /** automatic mode for filter (pid mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_PID = 2; + /** automatic mode for filter (tag mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_TAG = 3; + /** Manual filtering mode + new filter for debug app, if needed */ + public static final int FILTER_DEBUG = 4; + + public static final int COLUMN_MODE_MANUAL = 0; + public static final int COLUMN_MODE_AUTO = 1; + + public static String PREFS_TIME; + public static String PREFS_LEVEL; + public static String PREFS_PID; + public static String PREFS_TAG; + public static String PREFS_MESSAGE; + + /** + * This pattern is meant to parse the first line of a log message with the option + * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the + * following lines are the message (can be several line).<br> + * This first line looks something like<br> + * <code>"[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"</code> + * <br> + * Note: severity is one of V, D, I, W, or EM<br> + * Note: the fraction of second value can have any number of digit. + * Note the tag should be trim as it may have spaces at the end. + */ + private static Pattern sLogPattern = Pattern.compile( + "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$ + "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$ + + /** + * Interface for Storage Filter manager. Implementation of this interface + * provide a custom way to archive an reload filters. + */ + public interface ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore(); + + public void saveFilters(LogFilter[] filters); + + public boolean requiresDefaultFilter(); + } + + private Composite mParent; + private IPreferenceStore mStore; + + /** top object in the view */ + private TabFolder mFolders; + + private LogColors mColors; + + private ILogFilterStorageManager mFilterStorage; + + private LogCatOuputReceiver mCurrentLogCat; + + /** + * Circular buffer containing the logcat output. This is unfiltered. + * The valid content goes from <code>mBufferStart</code> to + * <code>mBufferEnd - 1</code>. Therefore its number of item is + * <code>mBufferEnd - mBufferStart</code>. + */ + private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH]; + + /** Represents the oldest message in the buffer */ + private int mBufferStart = -1; + + /** + * Represents the next usable item in the buffer to receive new message. + * This can be equal to mBufferStart, but when used mBufferStart will be + * incremented as well. + */ + private int mBufferEnd = -1; + + /** Filter list */ + private LogFilter[] mFilters; + + /** Default filter */ + private LogFilter mDefaultFilter; + + /** Current filter being displayed */ + private LogFilter mCurrentFilter; + + /** Filtering mode */ + private int mFilterMode = FILTER_NONE; + + /** Device currently running logcat */ + private 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 Binary files differnew file mode 100644 index 0000000..eefc2ca --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/add.png diff --git a/ddms/libs/ddmuilib/src/resources/images/android.png b/ddms/libs/ddmuilib/src/resources/images/android.png Binary files differnew file mode 100644 index 0000000..3779d4d --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/android.png diff --git a/ddms/libs/ddmuilib/src/resources/images/backward.png b/ddms/libs/ddmuilib/src/resources/images/backward.png Binary files differnew file mode 100644 index 0000000..90a9713 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/backward.png diff --git a/ddms/libs/ddmuilib/src/resources/images/clear.png b/ddms/libs/ddmuilib/src/resources/images/clear.png Binary files differnew file mode 100644 index 0000000..0009cf6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/clear.png diff --git a/ddms/libs/ddmuilib/src/resources/images/d.png b/ddms/libs/ddmuilib/src/resources/images/d.png Binary files differnew file mode 100644 index 0000000..d45506e --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/d.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-attach.png b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png Binary files differnew file mode 100644 index 0000000..9b8a11c --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-error.png b/ddms/libs/ddmuilib/src/resources/images/debug-error.png Binary files differnew file mode 100644 index 0000000..f22da1f --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-error.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-wait.png b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png Binary files differnew file mode 100644 index 0000000..322be63 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png diff --git a/ddms/libs/ddmuilib/src/resources/images/delete.png b/ddms/libs/ddmuilib/src/resources/images/delete.png Binary files differnew file mode 100644 index 0000000..db5fab8 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/delete.png diff --git a/ddms/libs/ddmuilib/src/resources/images/device.png b/ddms/libs/ddmuilib/src/resources/images/device.png Binary files differnew file mode 100644 index 0000000..7dbbbb6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/device.png diff --git a/ddms/libs/ddmuilib/src/resources/images/down.png b/ddms/libs/ddmuilib/src/resources/images/down.png Binary files differnew file mode 100644 index 0000000..f9426cb --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/down.png diff --git a/ddms/libs/ddmuilib/src/resources/images/e.png b/ddms/libs/ddmuilib/src/resources/images/e.png Binary files differnew file mode 100644 index 0000000..dee7c97 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/e.png diff --git a/ddms/libs/ddmuilib/src/resources/images/edit.png b/ddms/libs/ddmuilib/src/resources/images/edit.png Binary files differnew file mode 100644 index 0000000..b8f65bc --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/edit.png diff --git a/ddms/libs/ddmuilib/src/resources/images/empty.png b/ddms/libs/ddmuilib/src/resources/images/empty.png Binary files differnew file mode 100644 index 0000000..f021542 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/empty.png diff --git a/ddms/libs/ddmuilib/src/resources/images/emulator.png b/ddms/libs/ddmuilib/src/resources/images/emulator.png Binary files differnew file mode 100644 index 0000000..a718042 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/emulator.png diff --git a/ddms/libs/ddmuilib/src/resources/images/file.png b/ddms/libs/ddmuilib/src/resources/images/file.png Binary files differnew file mode 100644 index 0000000..043a814 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/file.png diff --git a/ddms/libs/ddmuilib/src/resources/images/folder.png b/ddms/libs/ddmuilib/src/resources/images/folder.png Binary files differnew file mode 100644 index 0000000..7e29b1a --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/folder.png diff --git a/ddms/libs/ddmuilib/src/resources/images/forward.png b/ddms/libs/ddmuilib/src/resources/images/forward.png Binary files differnew file mode 100644 index 0000000..a97a605 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/forward.png diff --git a/ddms/libs/ddmuilib/src/resources/images/gc.png b/ddms/libs/ddmuilib/src/resources/images/gc.png Binary files differnew file mode 100644 index 0000000..5194806 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/gc.png diff --git a/ddms/libs/ddmuilib/src/resources/images/halt.png b/ddms/libs/ddmuilib/src/resources/images/halt.png Binary files differnew file mode 100644 index 0000000..10e3720 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/halt.png diff --git a/ddms/libs/ddmuilib/src/resources/images/heap.png b/ddms/libs/ddmuilib/src/resources/images/heap.png Binary files differnew file mode 100644 index 0000000..e3aa3f0 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/heap.png diff --git a/ddms/libs/ddmuilib/src/resources/images/i.png b/ddms/libs/ddmuilib/src/resources/images/i.png Binary files differnew file mode 100644 index 0000000..98385c5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/i.png diff --git a/ddms/libs/ddmuilib/src/resources/images/importBug.png b/ddms/libs/ddmuilib/src/resources/images/importBug.png Binary files differnew file mode 100644 index 0000000..f5da179 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/importBug.png diff --git a/ddms/libs/ddmuilib/src/resources/images/load.png b/ddms/libs/ddmuilib/src/resources/images/load.png Binary files differnew file mode 100644 index 0000000..9e7bf6e --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/load.png diff --git a/ddms/libs/ddmuilib/src/resources/images/pause.png b/ddms/libs/ddmuilib/src/resources/images/pause.png Binary files differnew file mode 100644 index 0000000..19d286d --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/pause.png diff --git a/ddms/libs/ddmuilib/src/resources/images/play.png b/ddms/libs/ddmuilib/src/resources/images/play.png Binary files differnew file mode 100644 index 0000000..d54f013 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/play.png diff --git a/ddms/libs/ddmuilib/src/resources/images/pull.png b/ddms/libs/ddmuilib/src/resources/images/pull.png Binary files differnew file mode 100644 index 0000000..f48f1b1 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/pull.png diff --git a/ddms/libs/ddmuilib/src/resources/images/push.png b/ddms/libs/ddmuilib/src/resources/images/push.png Binary files differnew file mode 100644 index 0000000..6222864 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/push.png diff --git a/ddms/libs/ddmuilib/src/resources/images/save.png b/ddms/libs/ddmuilib/src/resources/images/save.png Binary files differnew file mode 100644 index 0000000..040ebda --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/save.png diff --git a/ddms/libs/ddmuilib/src/resources/images/thread.png b/ddms/libs/ddmuilib/src/resources/images/thread.png Binary files differnew file mode 100644 index 0000000..ac839e8 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/thread.png diff --git a/ddms/libs/ddmuilib/src/resources/images/up.png b/ddms/libs/ddmuilib/src/resources/images/up.png Binary files differnew file mode 100644 index 0000000..92edf5a --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/up.png diff --git a/ddms/libs/ddmuilib/src/resources/images/v.png b/ddms/libs/ddmuilib/src/resources/images/v.png Binary files differnew file mode 100644 index 0000000..8044051 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/v.png diff --git a/ddms/libs/ddmuilib/src/resources/images/w.png b/ddms/libs/ddmuilib/src/resources/images/w.png Binary files differnew file mode 100644 index 0000000..129d0f9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/w.png diff --git a/ddms/libs/ddmuilib/src/resources/images/warning.png b/ddms/libs/ddmuilib/src/resources/images/warning.png Binary files differnew file mode 100644 index 0000000..ca3b6ed --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/warning.png |