aboutsummaryrefslogtreecommitdiffstats
path: root/ddms/libs/ddmlib
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
commit55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch)
treeecd18b995aea8eeeb8b3823266280d41245bf0f7 /ddms/libs/ddmlib
parent82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff)
downloadsdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'ddms/libs/ddmlib')
-rw-r--r--ddms/libs/ddmlib/.classpath6
-rw-r--r--ddms/libs/ddmlib/.project17
-rw-r--r--ddms/libs/ddmlib/Android.mk4
-rw-r--r--ddms/libs/ddmlib/src/Android.mk11
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java714
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java71
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java1050
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java35
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java222
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Client.java768
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java502
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java147
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java72
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java351
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Device.java385
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java866
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java751
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java767
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java74
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java94
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java76
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java497
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java130
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java305
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java86
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java379
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java89
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java446
-rwxr-xr-xddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java184
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java44
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java29
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java371
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/Log.java351
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java780
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java130
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java277
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java73
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java95
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java50
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java32
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java949
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java139
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java461
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java577
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java214
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java347
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java74
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java78
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java247
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java85
-rwxr-xr-xddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java369
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java228
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java76
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java90
-rw-r--r--ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java245
-rw-r--r--ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java248
56 files changed, 15758 insertions, 0 deletions
diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath
new file mode 100644
index 0000000..fb50116
--- /dev/null
+++ b/ddms/libs/ddmlib/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/libs/ddmlib/.project b/ddms/libs/ddmlib/.project
new file mode 100644
index 0000000..fea25c7
--- /dev/null
+++ b/ddms/libs/ddmlib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ddmlib</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk
new file mode 100644
index 0000000..a49bdd2
--- /dev/null
+++ b/ddms/libs/ddmlib/Android.mk
@@ -0,0 +1,4 @@
+# Copyright 2007 The Android Open Source Project
+#
+DDMLIB_LOCAL_DIR := $(call my-dir)
+include $(DDMLIB_LOCAL_DIR)/src/Android.mk
diff --git a/ddms/libs/ddmlib/src/Android.mk b/ddms/libs/ddmlib/src/Android.mk
new file mode 100644
index 0000000..da07f97
--- /dev/null
+++ b/ddms/libs/ddmlib/src/Android.mk
@@ -0,0 +1,11 @@
+# Copyright 2007 The Android Open Source Project
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_MODULE := ddmlib
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java
new file mode 100644
index 0000000..42022fe
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SocketChannel;
+
+/**
+ * Helper class to handle requests and connections to adb.
+ * <p/>{@link DebugBridgeServer} is the public API to connection to adb, while {@link AdbHelper}
+ * does the low level stuff.
+ * <p/>This currently uses spin-wait non-blocking I/O. A Selector would be more efficient,
+ * but seems like overkill for what we're doing here.
+ */
+final class AdbHelper {
+
+ // public static final long kOkay = 0x59414b4fL;
+ // public static final long kFail = 0x4c494146L;
+
+ static final int WAIT_TIME = 5; // spin-wait sleep, in ms
+
+ public static final int STD_TIMEOUT = 5000; // standard delay, in ms
+
+ static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
+
+ /** do not instantiate */
+ private AdbHelper() {
+ }
+
+ /**
+ * Response from ADB.
+ */
+ static class AdbResponse {
+ public AdbResponse() {
+ // ioSuccess = okay = timeout = false;
+ message = "";
+ }
+
+ public boolean ioSuccess; // read all expected data, no timeoutes
+
+ public boolean okay; // first 4 bytes in response were "OKAY"?
+
+ public boolean timeout; // TODO: implement
+
+ public String message; // diagnostic string
+ }
+
+ /**
+ * Create and connect a new pass-through socket, from the host to a port on
+ * the device.
+ *
+ * @param adbSockAddr
+ * @param device the device to connect to. Can be null in which case the connection will be
+ * to the first available device.
+ * @param devicePort the port we're opening
+ */
+ public static SocketChannel open(InetSocketAddress adbSockAddr,
+ Device device, int devicePort) throws IOException {
+
+ SocketChannel adbChan = SocketChannel.open(adbSockAddr);
+ try {
+ adbChan.socket().setTcpNoDelay(true);
+ adbChan.configureBlocking(false);
+
+ // if the device is not -1, then we first tell adb we're looking to
+ // talk to a specific device
+ setDevice(adbChan, device);
+
+ byte[] req = createAdbForwardRequest(null, devicePort);
+ // Log.hexDump(req);
+
+ if (write(adbChan, req) == false)
+ throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
+
+ AdbResponse resp = readAdbResponse(adbChan, false);
+ if (!resp.okay)
+ throw new IOException("connection request rejected"); //$NON-NLS-1$
+
+ adbChan.configureBlocking(true);
+ } catch (IOException ioe) {
+ adbChan.close();
+ throw ioe;
+ }
+
+ return adbChan;
+ }
+
+ /**
+ * Creates and connects a new pass-through socket, from the host to a port on
+ * the device.
+ *
+ * @param adbSockAddr
+ * @param device the device to connect to. Can be null in which case the connection will be
+ * to the first available device.
+ * @param pid the process pid to connect to.
+ */
+ public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
+ Device device, int pid) throws IOException {
+
+ SocketChannel adbChan = SocketChannel.open(adbSockAddr);
+ try {
+ adbChan.socket().setTcpNoDelay(true);
+ adbChan.configureBlocking(false);
+
+ // if the device is not -1, then we first tell adb we're looking to
+ // talk to a specific device
+ setDevice(adbChan, device);
+
+ byte[] req = createJdwpForwardRequest(pid);
+ // Log.hexDump(req);
+
+ if (write(adbChan, req) == false)
+ throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.okay)
+ throw new IOException("connection request rejected: " + resp.message); //$NON-NLS-1$
+
+ adbChan.configureBlocking(true);
+ } catch (IOException ioe) {
+ adbChan.close();
+ throw ioe;
+ }
+
+ return adbChan;
+ }
+
+ /**
+ * Creates a port forwarding request for adb. This returns an array
+ * containing "####tcp:{port}:{addStr}".
+ * @param addrStr the host. Can be null.
+ * @param port the port on the device. This does not need to be numeric.
+ */
+ private static byte[] createAdbForwardRequest(String addrStr, int port) {
+ String reqStr;
+
+ if (addrStr == null)
+ reqStr = "tcp:" + port;
+ else
+ reqStr = "tcp:" + port + ":" + addrStr;
+ return formAdbRequest(reqStr);
+ }
+
+ /**
+ * Creates a port forwarding request to a jdwp process. This returns an array
+ * containing "####jwdp:{pid}".
+ * @param pid the jdwp process pid on the device.
+ */
+ private static byte[] createJdwpForwardRequest(int pid) {
+ String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$
+ return formAdbRequest(reqStr);
+ }
+
+ /**
+ * Create an ASCII string preceeded by four hex digits. The opening "####"
+ * is the length of the rest of the string, encoded as ASCII hex (case
+ * doesn't matter). "port" and "host" are what we want to forward to. If
+ * we're on the host side connecting into the device, "addrStr" should be
+ * null.
+ */
+ static byte[] formAdbRequest(String req) {
+ String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$
+ byte[] result;
+ try {
+ result = resultStr.getBytes(DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace(); // not expected
+ return null;
+ }
+ assert result.length == req.length() + 4;
+ return result;
+ }
+
+ /**
+ * Reads the response from ADB after a command.
+ * @param chan The socket channel that is connected to adb.
+ * @param readDiagString If true, we're expecting an OKAY response to be
+ * followed by a diagnostic string. Otherwise, we only expect the
+ * diagnostic string to follow a FAIL.
+ */
+ static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
+ throws IOException {
+
+ AdbResponse resp = new AdbResponse();
+
+ byte[] reply = new byte[4];
+ if (read(chan, reply) == false) {
+ return resp;
+ }
+ resp.ioSuccess = true;
+
+ if (isOkay(reply)) {
+ resp.okay = true;
+ } else {
+ readDiagString = true; // look for a reason after the FAIL
+ resp.okay = false;
+ }
+
+ // not a loop -- use "while" so we can use "break"
+ while (readDiagString) {
+ // length string is in next 4 bytes
+ byte[] lenBuf = new byte[4];
+ if (read(chan, lenBuf) == false) {
+ Log.w("ddms", "Expected diagnostic string not found");
+ break;
+ }
+
+ String lenStr = replyToString(lenBuf);
+
+ int len;
+ try {
+ len = Integer.parseInt(lenStr, 16);
+ } catch (NumberFormatException nfe) {
+ Log.w("ddms", "Expected digits, got '" + lenStr + "': "
+ + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " "
+ + lenBuf[3]);
+ Log.w("ddms", "reply was " + replyToString(reply));
+ break;
+ }
+
+ byte[] msg = new byte[len];
+ if (read(chan, msg) == false) {
+ Log.w("ddms", "Failed reading diagnostic string, len=" + len);
+ break;
+ }
+
+ resp.message = replyToString(msg);
+ Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
+ + resp.message + "'");
+
+ break;
+ }
+
+ return resp;
+ }
+
+ /**
+ * Retrieve the frame buffer from the device.
+ */
+ public static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
+ throws IOException {
+
+ RawImage imageParams = new RawImage();
+ byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$
+ byte[] nudge = {
+ 0
+ };
+ byte[] reply;
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ // if the device is not -1, then we first tell adb we're looking to talk
+ // to a specific device
+ setDevice(adbChan, device);
+
+ if (write(adbChan, request) == false)
+ throw new IOException("failed asking for frame buffer");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.w("ddms", "Got timeout or unhappy response from ADB fb req: "
+ + resp.message);
+ adbChan.close();
+ return null;
+ }
+
+ reply = new byte[16];
+ if (read(adbChan, reply) == false) {
+ Log.w("ddms", "got partial reply from ADB fb:");
+ Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length);
+ adbChan.close();
+ return null;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(reply);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ imageParams.bpp = buf.getInt();
+ imageParams.size = buf.getInt();
+ imageParams.width = buf.getInt();
+ imageParams.height = buf.getInt();
+
+ Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
+ + imageParams.size + ", width=" + imageParams.width
+ + ", height=" + imageParams.height);
+
+ if (write(adbChan, nudge) == false)
+ throw new IOException("failed nudging");
+
+ reply = new byte[imageParams.size];
+ if (read(adbChan, reply) == false) {
+ Log.w("ddms", "got truncated reply from ADB fb data");
+ adbChan.close();
+ return null;
+ }
+ imageParams.data = reply;
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return imageParams;
+ }
+
+ /**
+ * Execute a command on the device and retrieve the output. The output is
+ * handed to "rcvr" as it arrives.
+ */
+ public static void executeRemoteCommand(InetSocketAddress adbSockAddr,
+ String command, Device device, IShellOutputReceiver rcvr)
+ throws IOException {
+ Log.v("ddms", "execute: running " + command);
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ // if the device is not -1, then we first tell adb we're looking to
+ // talk
+ // to a specific device
+ setDevice(adbChan, device);
+
+ byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$
+ if (write(adbChan, request) == false)
+ throw new IOException("failed submitting shell command");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);
+ throw new IOException("sad result from adb: " + resp.message);
+ }
+
+ byte[] data = new byte[16384];
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ while (true) {
+ int count;
+
+ if (rcvr != null && rcvr.isCancelled()) {
+ Log.v("ddms", "execute: cancelled");
+ break;
+ }
+
+ count = adbChan.read(buf);
+ if (count < 0) {
+ // we're at the end, we flush the output
+ rcvr.flush();
+ Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: "
+ + count);
+ break;
+ } else if (count == 0) {
+ try {
+ Thread.sleep(WAIT_TIME * 5);
+ } catch (InterruptedException ie) {
+ }
+ } else {
+ if (rcvr != null) {
+ rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position());
+ }
+ buf.rewind();
+ }
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ Log.v("ddms", "execute: returning");
+ }
+ }
+
+ /**
+ * Runs the Event log service on the {@link Device}, and provides its output to the
+ * {@link LogReceiver}.
+ * @param adbSockAddr the socket address to connect to adb
+ * @param device the Device on which to run the service
+ * @param rcvr the {@link LogReceiver} to receive the log output
+ * @throws IOException
+ */
+ public static void runEventLogService(InetSocketAddress adbSockAddr, Device device,
+ LogReceiver rcvr) throws IOException {
+ runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$
+ }
+
+ /**
+ * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}.
+ * @param adbSockAddr the socket address to connect to adb
+ * @param device the Device on which to run the service
+ * @param logName the name of the log file to output
+ * @param rcvr the {@link LogReceiver} to receive the log output
+ * @throws IOException
+ */
+ public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName,
+ LogReceiver rcvr) throws IOException {
+ SocketChannel adbChan = null;
+
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ // if the device is not -1, then we first tell adb we're looking to talk
+ // to a specific device
+ setDevice(adbChan, device);
+
+ byte[] request = formAdbRequest("log:" + logName);
+ if (write(adbChan, request) == false) {
+ throw new IOException("failed to submit the log command");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected log command: " + resp.message);
+ }
+
+ byte[] data = new byte[16384];
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ while (true) {
+ int count;
+
+ if (rcvr != null && rcvr.isCancelled()) {
+ break;
+ }
+
+ count = adbChan.read(buf);
+ if (count < 0) {
+ break;
+ } else if (count == 0) {
+ try {
+ Thread.sleep(WAIT_TIME * 5);
+ } catch (InterruptedException ie) {
+ }
+ } else {
+ if (rcvr != null) {
+ rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position());
+ }
+ buf.rewind();
+ }
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+ }
+
+ /**
+ * Creates a port forwarding between a local and a remote port.
+ * @param adbSockAddr the socket address to connect to adb
+ * @param device the device on which to do the port fowarding
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ * @throws IOException
+ */
+ public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort,
+ int remotePort) throws IOException {
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ byte[] request = formAdbRequest(String.format(
+ "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
+ device.serialNumber, localPort, remotePort));
+
+ if (write(adbChan, request) == false) {
+ throw new IOException("failed to submit the forward command.");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected command: " + resp.message);
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove a port forwarding between a local and a remote port.
+ * @param adbSockAddr the socket address to connect to adb
+ * @param device the device on which to remove the port fowarding
+ * @param localPort the local port of the forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ * @throws IOException
+ */
+ public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort,
+ int remotePort) throws IOException {
+
+ SocketChannel adbChan = null;
+ try {
+ adbChan = SocketChannel.open(adbSockAddr);
+ adbChan.configureBlocking(false);
+
+ byte[] request = formAdbRequest(String.format(
+ "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
+ device.serialNumber, localPort, remotePort));
+
+ if (!write(adbChan, request)) {
+ throw new IOException("failed to submit the remove forward command.");
+ }
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.ioSuccess || !resp.okay) {
+ throw new IOException("Device rejected command: " + resp.message);
+ }
+ } finally {
+ if (adbChan != null) {
+ adbChan.close();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks to see if the first four bytes in "reply" are OKAY.
+ */
+ static boolean isOkay(byte[] reply) {
+ return reply[0] == (byte)'O' && reply[1] == (byte)'K'
+ && reply[2] == (byte)'A' && reply[3] == (byte)'Y';
+ }
+
+ /**
+ * Converts an ADB reply to a string.
+ */
+ static String replyToString(byte[] reply) {
+ String result;
+ try {
+ result = new String(reply, DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace(); // not expected
+ result = "";
+ }
+ return result;
+ }
+
+ /**
+ * Reads from the socket until the array is filled, or no more data is coming (because
+ * the socket closed or the timeout expired).
+ *
+ * @param chan the opened socket to read from. It must be in non-blocking
+ * mode for timeouts to work
+ * @param data the buffer to store the read data into.
+ * @return "true" if all data was read.
+ * @throws IOException
+ */
+ static boolean read(SocketChannel chan, byte[] data) {
+ try {
+ read(chan, data, -1, STD_TIMEOUT);
+ } catch (IOException e) {
+ Log.d("ddms", "readAll: IOException: " + e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Reads from the socket until the array is filled, the optional length
+ * is reached, or no more data is coming (because the socket closed or the
+ * timeout expired). After "timeout" milliseconds since the
+ * previous successful read, this will return whether or not new data has
+ * been found.
+ *
+ * @param chan the opened socket to read from. It must be in non-blocking
+ * mode for timeouts to work
+ * @param data the buffer to store the read data into.
+ * @param length the length to read or -1 to fill the data buffer completely
+ * @param timeout The timeout value. A timeout of zero means "wait forever".
+ * @throws IOException
+ */
+ static void read(SocketChannel chan, byte[] data, int length, int timeout) throws IOException {
+ ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
+ int numWaits = 0;
+
+ while (buf.position() != buf.limit()) {
+ int count;
+
+ count = chan.read(buf);
+ if (count < 0) {
+ Log.d("ddms", "read: channel EOF");
+ throw new IOException("EOF");
+ } else if (count == 0) {
+ // TODO: need more accurate timeout?
+ if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
+ Log.i("ddms", "read: timeout");
+ throw new IOException("timeout");
+ }
+ // non-blocking spin
+ try {
+ Thread.sleep(WAIT_TIME);
+ } catch (InterruptedException ie) {
+ }
+ numWaits++;
+ } else {
+ numWaits = 0;
+ }
+ }
+ }
+
+ /**
+ * Write until all data in "data" is written or the connection fails.
+ * @param chan the opened socket to write to.
+ * @param data the buffer to send.
+ * @return "true" if all data was written.
+ */
+ static boolean write(SocketChannel chan, byte[] data) {
+ try {
+ write(chan, data, -1, STD_TIMEOUT);
+ } catch (IOException e) {
+ Log.e("ddms", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write until all data in "data" is written, the optional length is reached,
+ * the timeout expires, or the connection fails. Returns "true" if all
+ * data was written.
+ * @param chan the opened socket to write to.
+ * @param data the buffer to send.
+ * @param length the length to write or -1 to send the whole buffer.
+ * @param timeout The timeout value. A timeout of zero means "wait forever".
+ * @throws IOException
+ */
+ static void write(SocketChannel chan, byte[] data, int length, int timeout)
+ throws IOException {
+ ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
+ int numWaits = 0;
+
+ while (buf.position() != buf.limit()) {
+ int count;
+
+ count = chan.write(buf);
+ if (count < 0) {
+ Log.d("ddms", "write: channel EOF");
+ throw new IOException("channel EOF");
+ } else if (count == 0) {
+ // TODO: need more accurate timeout?
+ if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
+ Log.i("ddms", "write: timeout");
+ throw new IOException("timeout");
+ }
+ // non-blocking spin
+ try {
+ Thread.sleep(WAIT_TIME);
+ } catch (InterruptedException ie) {
+ }
+ numWaits++;
+ } else {
+ numWaits = 0;
+ }
+ }
+ }
+
+ /**
+ * tells adb to talk to a specific device
+ *
+ * @param adbChan the socket connection to adb
+ * @param device The device to talk to.
+ * @throws IOException
+ */
+ static void setDevice(SocketChannel adbChan, Device device)
+ throws IOException {
+ // if the device is not -1, then we first tell adb we're looking to talk
+ // to a specific device
+ if (device != null) {
+ String msg = "host:transport:" + device.serialNumber; //$NON-NLS-1$
+ byte[] device_query = formAdbRequest(msg);
+
+ if (write(adbChan, device_query) == false)
+ throw new IOException("failed submitting device (" + device +
+ ") request to ADB");
+
+ AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+ if (!resp.okay)
+ throw new IOException("device (" + device +
+ ") request rejected: " + resp.message);
+ }
+
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java
new file mode 100644
index 0000000..c6d4b50
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Holds an Allocation information.
+ */
+public class AllocationInfo implements Comparable<AllocationInfo>, IStackTraceInfo {
+ private String mAllocatedClass;
+ private int mAllocationSize;
+ private short mThreadId;
+ private StackTraceElement[] mStackTrace;
+
+ /*
+ * Simple constructor.
+ */
+ AllocationInfo(String allocatedClass, int allocationSize,
+ short threadId, StackTraceElement[] stackTrace) {
+ mAllocatedClass = allocatedClass;
+ mAllocationSize = allocationSize;
+ mThreadId = threadId;
+ mStackTrace = stackTrace;
+ }
+
+ /**
+ * Returns the name of the allocated class.
+ */
+ public String getAllocatedClass() {
+ return mAllocatedClass;
+ }
+
+ /**
+ * Returns the size of the allocation.
+ */
+ public int getSize() {
+ return mAllocationSize;
+ }
+
+ /**
+ * Returns the id of the thread that performed the allocation.
+ */
+ public short getThreadId() {
+ return mThreadId;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
+ */
+ public StackTraceElement[] getStackTrace() {
+ return mStackTrace;
+ }
+
+ public int compareTo(AllocationInfo otherAlloc) {
+ return otherAlloc.mAllocationSize - mAllocationSize;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java
new file mode 100644
index 0000000..795bf88
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java
@@ -0,0 +1,1050 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.Thread.State;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A connection to the host-side android debug bridge (adb)
+ * <p/>This is the central point to communicate with any devices, emulators, or the applications
+ * running on them.
+ * <p/><b>{@link #init(boolean)} must be called before anything is done.</b>
+ */
+public final class AndroidDebugBridge {
+
+ /*
+ * Minimum and maximum version of adb supported. This correspond to
+ * ADB_SERVER_VERSION found in //device/tools/adb/adb.h
+ */
+
+ private final static int ADB_VERSION_MICRO_MIN = 20;
+ private final static int ADB_VERSION_MICRO_MAX = -1;
+
+ private final static Pattern sAdbVersion = Pattern.compile(
+ "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$
+
+ private final static String ADB = "adb"; //$NON-NLS-1$
+ private final static String DDMS = "ddms"; //$NON-NLS-1$
+
+ // Where to find the ADB bridge.
+ final static String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$
+ final static int ADB_PORT = 5037;
+
+ static InetAddress sHostAddr;
+ static InetSocketAddress sSocketAddr;
+
+ static {
+ // built-in local address/port for ADB.
+ try {
+ sHostAddr = InetAddress.getByName(ADB_HOST);
+ sSocketAddr = new InetSocketAddress(sHostAddr, ADB_PORT);
+ } catch (UnknownHostException e) {
+
+ }
+ }
+
+ private static AndroidDebugBridge sThis;
+ private static boolean sClientSupport;
+
+ /** Full path to adb. */
+ private String mAdbOsLocation = null;
+
+ private boolean mVersionCheck;
+
+ private boolean mStarted = false;
+
+ private DeviceMonitor mDeviceMonitor;
+
+ private final static ArrayList<IDebugBridgeChangeListener> sBridgeListeners =
+ new ArrayList<IDebugBridgeChangeListener>();
+ private final static ArrayList<IDeviceChangeListener> sDeviceListeners =
+ new ArrayList<IDeviceChangeListener>();
+ private final static ArrayList<IClientChangeListener> sClientListeners =
+ new ArrayList<IClientChangeListener>();
+
+ // lock object for synchronization
+ private static final Object sLock = sBridgeListeners;
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with {@link AndroidDebugBridge} changes.
+ */
+ public interface IDebugBridgeChangeListener {
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is connected.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ */
+ public void bridgeChanged(AndroidDebugBridge bridge);
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deal
+ * with {@link Device} addition, deletion, and changes.
+ */
+ public interface IDeviceChangeListener {
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ */
+ public void deviceConnected(Device device);
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ */
+ public void deviceDisconnected(Device device);
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask describing what changed. It can contain any of the following
+ * values: {@link Device#CHANGE_BUILD_INFO}, {@link Device#CHANGE_STATE},
+ * {@link Device#CHANGE_CLIENT_LIST}
+ */
+ public void deviceChanged(Device device, int changeMask);
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deal
+ * with {@link Client} changes.
+ */
+ public interface IClientChangeListener {
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO},
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ */
+ public void clientChanged(Client client, int changeMask);
+ }
+
+ /**
+ * Initializes the <code>ddm</code> library.
+ * <p/>This must be called once <b>before</b> any call to
+ * {@link #createBridge(String, boolean)}.
+ * <p>The library can be initialized in 2 ways:
+ * <ul>
+ * <li>Mode 1: <var>clientSupport</var> == <code>true</code>.<br>The library monitors the
+ * devices and the applications running on them. It will connect to each application, as a
+ * debugger of sort, to be able to interact with them through JDWP packets.</li>
+ * <li>Mode 2: <var>clientSupport</var> == <code>false</code>.<br>The library only monitors
+ * devices. The applications are left untouched, letting other tools built on
+ * <code>ddmlib</code> to connect a debugger to them.</li>
+ * </ul>
+ * <p/><b>Only one tool can run in mode 1 at the same time.</b>
+ * <p/>Note that mode 1 does not prevent debugging of applications running on devices. Mode 1
+ * lets debuggers connect to <code>ddmlib</code> which acts as a proxy between the debuggers and
+ * the applications to debug. See {@link Client#getDebuggerListenPort()}.
+ * <p/>The preferences of <code>ddmlib</code> should also be initialized with whatever default
+ * values were changed from the default values.
+ * <p/>When the application quits, {@link #terminate()} should be called.
+ * @param clientSupport Indicates whether the library should enable the monitoring and
+ * interaction with applications running on the devices.
+ * @see AndroidDebugBridge#createBridge(String, boolean)
+ * @see DdmPreferences
+ */
+ public static void init(boolean clientSupport) {
+ sClientSupport = clientSupport;
+
+ MonitorThread monitorThread = MonitorThread.createInstance();
+ monitorThread.start();
+
+ HandleHello.register(monitorThread);
+ HandleAppName.register(monitorThread);
+ HandleTest.register(monitorThread);
+ HandleThread.register(monitorThread);
+ HandleHeap.register(monitorThread);
+ HandleWait.register(monitorThread);
+ }
+
+ /**
+ * Terminates the ddm library. This must be called upon application termination.
+ */
+ public static void terminate() {
+ // kill the monitoring services
+ if (sThis != null && sThis.mDeviceMonitor != null) {
+ sThis.mDeviceMonitor.stop();
+ sThis.mDeviceMonitor = null;
+ }
+
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (monitorThread != null) {
+ monitorThread.quit();
+ }
+ }
+
+ /**
+ * Returns whether the ddmlib is setup to support monitoring and interacting with
+ * {@link Client}s running on the {@link Device}s.
+ */
+ static boolean getClientSupport() {
+ return sClientSupport;
+ }
+
+ /**
+ * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable.
+ * <p/>This bridge will expect adb to be running. It will not be able to start/stop/restart
+ * adb.
+ * <p/>If a bridge has already been started, it is directly returned with no changes (similar
+ * to calling {@link #getBridge()}).
+ * @return a connected bridge.
+ */
+ public static AndroidDebugBridge createBridge() {
+ synchronized (sLock) {
+ if (sThis != null) {
+ return sThis;
+ }
+
+ try {
+ sThis = new AndroidDebugBridge();
+ sThis.start();
+ } catch (InvalidParameterException e) {
+ sThis = null;
+ }
+
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+ new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+ // notify the listeners of the change
+ for (IDebugBridgeChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.bridgeChanged(sThis);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+
+ return sThis;
+ }
+ }
+
+
+ /**
+ * Creates a new debug bridge from the location of the command line tool.
+ * <p/>
+ * Any existing server will be disconnected, unless the location is the same and
+ * <code>forceNewBridge</code> is set to false.
+ * @param osLocation the location of the command line tool 'adb'
+ * @param forceNewBridge force creation of a new bridge even if one with the same location
+ * already exists.
+ * @return a connected bridge.
+ */
+ public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) {
+ synchronized (sLock) {
+ if (sThis != null) {
+ if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) &&
+ forceNewBridge == false) {
+ return sThis;
+ } else {
+ // stop the current server
+ sThis.stop();
+ }
+ }
+
+ try {
+ sThis = new AndroidDebugBridge(osLocation);
+ sThis.start();
+ } catch (InvalidParameterException e) {
+ sThis = null;
+ }
+
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+ new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+ // notify the listeners of the change
+ for (IDebugBridgeChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.bridgeChanged(sThis);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+
+ return sThis;
+ }
+ }
+
+ /**
+ * Returns the current debug bridge. Can be <code>null</code> if none were created.
+ */
+ public static AndroidDebugBridge getBridge() {
+ return sThis;
+ }
+
+ /**
+ * Disconnects the current debug bridge, and destroy the object.
+ * <p/>
+ * A new object will have to be created with {@link #createBridge(String, boolean)}.
+ */
+ public static void disconnectBridge() {
+ synchronized (sLock) {
+ if (sThis != null) {
+ sThis.stop();
+ sThis = null;
+
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+ new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+ // notify the listeners.
+ for (IDebugBridgeChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.bridgeChanged(sThis);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the listener to the collection of listeners who will be notified when a new
+ * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined
+ * in the {@link IDebugBridgeChangeListener} interface.
+ * @param listener The listener which should be notified.
+ */
+ public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) {
+ synchronized (sLock) {
+ if (sBridgeListeners.contains(listener) == false) {
+ sBridgeListeners.add(listener);
+ if (sThis != null) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.bridgeChanged(sThis);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the listener from the collection of listeners who will be notified when a new
+ * {@link AndroidDebugBridge} is started.
+ * @param listener The listener which should no longer be notified.
+ */
+ public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) {
+ synchronized (sLock) {
+ sBridgeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds the listener to the collection of listeners who will be notified when a {@link Device}
+ * is connected, disconnected, or when its properties or its {@link Client} list changed,
+ * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface.
+ * @param listener The listener which should be notified.
+ */
+ public static void addDeviceChangeListener(IDeviceChangeListener listener) {
+ synchronized (sLock) {
+ if (sDeviceListeners.contains(listener) == false) {
+ sDeviceListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes the listener from the collection of listeners who will be notified when a
+ * {@link Device} is connected, disconnected, or when its properties or its {@link Client}
+ * list changed.
+ * @param listener The listener which should no longer be notified.
+ */
+ public static void removeDeviceChangeListener(IDeviceChangeListener listener) {
+ synchronized (sLock) {
+ sDeviceListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds the listener to the collection of listeners who will be notified when a {@link Client}
+ * property changed, by sending it one of the messages defined in the
+ * {@link IClientChangeListener} interface.
+ * @param listener The listener which should be notified.
+ */
+ public static void addClientChangeListener(IClientChangeListener listener) {
+ synchronized (sLock) {
+ if (sClientListeners.contains(listener) == false) {
+ sClientListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes the listener from the collection of listeners who will be notified when a
+ * {@link Client} property changed.
+ * @param listener The listener which should no longer be notified.
+ */
+ public static void removeClientChangeListener(IClientChangeListener listener) {
+ synchronized (sLock) {
+ sClientListeners.remove(listener);
+ }
+ }
+
+
+ /**
+ * Returns the devices.
+ * @see #hasInitialDeviceList()
+ */
+ public Device[] getDevices() {
+ synchronized (sLock) {
+ if (mDeviceMonitor != null) {
+ return mDeviceMonitor.getDevices();
+ }
+ }
+
+ return new Device[0];
+ }
+
+ /**
+ * Returns whether the bridge has acquired the initial list from adb after being created.
+ * <p/>Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will
+ * generally result in an empty list. This is due to the internal asynchronous communication
+ * mechanism with <code>adb</code> that does not guarantee that the {@link Device} list has been
+ * built before the call to {@link #getDevices()}.
+ * <p/>The recommended way to get the list of {@link Device} objects is to create a
+ * {@link IDeviceChangeListener} object.
+ */
+ public boolean hasInitialDeviceList() {
+ if (mDeviceMonitor != null) {
+ return mDeviceMonitor.hasInitialDeviceList();
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the client to accept debugger connection on the custom "Selected debug port".
+ * @param selectedClient the client. Can be null.
+ */
+ public void setSelectedClient(Client selectedClient) {
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (monitorThread != null) {
+ monitorThread.setSelectedClient(selectedClient);
+ }
+ }
+
+ /**
+ * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon.
+ */
+ public boolean isConnected() {
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (mDeviceMonitor != null && monitorThread != null) {
+ return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect
+ * to the adb daemon.
+ */
+ public int getConnectionAttemptCount() {
+ if (mDeviceMonitor != null) {
+ return mDeviceMonitor.getConnectionAttemptCount();
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart
+ * the adb daemon.
+ */
+ public int getRestartAttemptCount() {
+ if (mDeviceMonitor != null) {
+ return mDeviceMonitor.getRestartAttemptCount();
+ }
+ return -1;
+ }
+
+ /**
+ * Creates a new bridge.
+ * @param osLocation the location of the command line tool
+ * @throws InvalidParameterException
+ */
+ private AndroidDebugBridge(String osLocation) throws InvalidParameterException {
+ if (osLocation == null || osLocation.length() == 0) {
+ throw new InvalidParameterException();
+ }
+ mAdbOsLocation = osLocation;
+
+ checkAdbVersion();
+ }
+
+ /**
+ * Creates a new bridge not linked to any particular adb executable.
+ */
+ private AndroidDebugBridge() {
+ }
+
+ /**
+ * Queries adb for its version number and checks it against {@link #MIN_VERSION_NUMBER} and
+ * {@link #MAX_VERSION_NUMBER}
+ */
+ private void checkAdbVersion() {
+ // default is bad check
+ mVersionCheck = false;
+
+ if (mAdbOsLocation == null) {
+ return;
+ }
+
+ try {
+ String[] command = new String[2];
+ command[0] = mAdbOsLocation;
+ command[1] = "version"; //$NON-NLS-1$
+ Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation)); //$NON-NLS-1$
+ Process process = Runtime.getRuntime().exec(command);
+
+ ArrayList<String> errorOutput = new ArrayList<String>();
+ ArrayList<String> stdOutput = new ArrayList<String>();
+ int status = grabProcessOutput(process, errorOutput, stdOutput,
+ true /* waitForReaders */);
+
+ if (status != 0) {
+ StringBuilder builder = new StringBuilder("'adb version' failed!"); //$NON-NLS-1$
+ for (String error : errorOutput) {
+ builder.append('\n');
+ builder.append(error);
+ }
+ Log.logAndDisplay(LogLevel.ERROR, "adb", builder.toString());
+ }
+
+ // check both stdout and stderr
+ boolean versionFound = false;
+ for (String line : stdOutput) {
+ versionFound = scanVersionLine(line);
+ if (versionFound) {
+ break;
+ }
+ }
+ if (!versionFound) {
+ for (String line : errorOutput) {
+ versionFound = scanVersionLine(line);
+ if (versionFound) {
+ break;
+ }
+ }
+ }
+
+ if (!versionFound) {
+ // if we get here, we failed to parse the output.
+ Log.logAndDisplay(LogLevel.ERROR, ADB,
+ "Failed to parse the output of 'adb version'"); //$NON-NLS-1$
+ }
+
+ } catch (IOException e) {
+ Log.logAndDisplay(LogLevel.ERROR, ADB,
+ "Failed to get the adb version: " + e.getMessage()); //$NON-NLS-1$
+ } catch (InterruptedException e) {
+ } finally {
+
+ }
+ }
+
+ /**
+ * Scans a line resulting from 'adb version' for a potential version number.
+ * <p/>
+ * If a version number is found, it checks the version number against what is expected
+ * by this version of ddms.
+ * <p/>
+ * Returns true when a version number has been found so that we can stop scanning,
+ * whether the version number is in the acceptable range or not.
+ *
+ * @param line The line to scan.
+ * @return True if a version number was found (whether it is acceptable or not).
+ */
+ private boolean scanVersionLine(String line) {
+ if (line != null) {
+ Matcher matcher = sAdbVersion.matcher(line);
+ if (matcher.matches()) {
+ int majorVersion = Integer.parseInt(matcher.group(1));
+ int minorVersion = Integer.parseInt(matcher.group(2));
+ int microVersion = Integer.parseInt(matcher.group(3));
+
+ // check only the micro version for now.
+ if (microVersion < ADB_VERSION_MICRO_MIN) {
+ String message = String.format(
+ "Required minimum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$
+ + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$
+ majorVersion, minorVersion, ADB_VERSION_MICRO_MIN,
+ microVersion);
+ Log.logAndDisplay(LogLevel.ERROR, ADB, message);
+ } else if (ADB_VERSION_MICRO_MAX != -1 &&
+ microVersion > ADB_VERSION_MICRO_MAX) {
+ String message = String.format(
+ "Required maximum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$
+ + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$
+ majorVersion, minorVersion, ADB_VERSION_MICRO_MAX,
+ microVersion);
+ Log.logAndDisplay(LogLevel.ERROR, ADB, message);
+ } else {
+ mVersionCheck = true;
+ }
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Starts the debug bridge.
+ * @return true if success.
+ */
+ boolean start() {
+ if (mAdbOsLocation != null && (mVersionCheck == false || startAdb() == false)) {
+ return false;
+ }
+
+ mStarted = true;
+
+ // now that the bridge is connected, we start the underlying services.
+ mDeviceMonitor = new DeviceMonitor(this);
+ mDeviceMonitor.start();
+
+ return true;
+ }
+
+ /**
+ * Kills the debug bridge.
+ * @return true if success
+ */
+ boolean stop() {
+ // if we haven't started we return false;
+ if (mStarted == false) {
+ return false;
+ }
+
+ // kill the monitoring services
+ mDeviceMonitor.stop();
+ mDeviceMonitor = null;
+
+ if (stopAdb() == false) {
+ return false;
+ }
+
+ mStarted = false;
+ return true;
+ }
+
+ /**
+ * Restarts adb, but not the services around it.
+ * @return true if success.
+ */
+ public boolean restart() {
+ if (mAdbOsLocation == null) {
+ Log.e(ADB,
+ "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+ return false;
+ }
+
+ if (mVersionCheck == false) {
+ Log.logAndDisplay(LogLevel.ERROR, ADB,
+ "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$
+ return false;
+ }
+ synchronized (this) {
+ stopAdb();
+
+ boolean restart = startAdb();
+
+ if (restart && mDeviceMonitor == null) {
+ mDeviceMonitor = new DeviceMonitor(this);
+ mDeviceMonitor.start();
+ }
+
+ return restart;
+ }
+ }
+
+ /**
+ * Notify the listener of a new {@link Device}.
+ * <p/>
+ * The notification of the listeners is done in a synchronized block. It is important to
+ * expect the listeners to potentially access various methods of {@link Device} as well as
+ * {@link #getDevices()} which use internal locks.
+ * <p/>
+ * For this reason, any call to this method from a method of {@link DeviceMonitor},
+ * {@link Device} which is also inside a synchronized block, should first synchronize on
+ * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+ * @param device the new <code>Device</code>.
+ * @see #getLock()
+ */
+ void deviceConnected(Device device) {
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDeviceChangeListener[] listenersCopy = null;
+ synchronized (sLock) {
+ listenersCopy = sDeviceListeners.toArray(
+ new IDeviceChangeListener[sDeviceListeners.size()]);
+ }
+
+ // Notify the listeners
+ for (IDeviceChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.deviceConnected(device);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+
+ /**
+ * Notify the listener of a disconnected {@link Device}.
+ * <p/>
+ * The notification of the listeners is done in a synchronized block. It is important to
+ * expect the listeners to potentially access various methods of {@link Device} as well as
+ * {@link #getDevices()} which use internal locks.
+ * <p/>
+ * For this reason, any call to this method from a method of {@link DeviceMonitor},
+ * {@link Device} which is also inside a synchronized block, should first synchronize on
+ * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+ * @param device the disconnected <code>Device</code>.
+ * @see #getLock()
+ */
+ void deviceDisconnected(Device device) {
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDeviceChangeListener[] listenersCopy = null;
+ synchronized (sLock) {
+ listenersCopy = sDeviceListeners.toArray(
+ new IDeviceChangeListener[sDeviceListeners.size()]);
+ }
+
+ // Notify the listeners
+ for (IDeviceChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.deviceDisconnected(device);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+
+ /**
+ * Notify the listener of a modified {@link Device}.
+ * <p/>
+ * The notification of the listeners is done in a synchronized block. It is important to
+ * expect the listeners to potentially access various methods of {@link Device} as well as
+ * {@link #getDevices()} which use internal locks.
+ * <p/>
+ * For this reason, any call to this method from a method of {@link DeviceMonitor},
+ * {@link Device} which is also inside a synchronized block, should first synchronize on
+ * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+ * @param device the modified <code>Device</code>.
+ * @see #getLock()
+ */
+ void deviceChanged(Device device, int changeMask) {
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IDeviceChangeListener[] listenersCopy = null;
+ synchronized (sLock) {
+ listenersCopy = sDeviceListeners.toArray(
+ new IDeviceChangeListener[sDeviceListeners.size()]);
+ }
+
+ // Notify the listeners
+ for (IDeviceChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.deviceChanged(device, changeMask);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+
+ /**
+ * Notify the listener of a modified {@link Client}.
+ * <p/>
+ * The notification of the listeners is done in a synchronized block. It is important to
+ * expect the listeners to potentially access various methods of {@link Device} as well as
+ * {@link #getDevices()} which use internal locks.
+ * <p/>
+ * For this reason, any call to this method from a method of {@link DeviceMonitor},
+ * {@link Device} which is also inside a synchronized block, should first synchronize on
+ * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+ * @param device the modified <code>Client</code>.
+ * @param changeMask the mask indicating what changed in the <code>Client</code>
+ * @see #getLock()
+ */
+ void clientChanged(Client client, int changeMask) {
+ // because the listeners could remove themselves from the list while processing
+ // their event callback, we make a copy of the list and iterate on it instead of
+ // the main list.
+ // This mostly happens when the application quits.
+ IClientChangeListener[] listenersCopy = null;
+ synchronized (sLock) {
+ listenersCopy = sClientListeners.toArray(
+ new IClientChangeListener[sClientListeners.size()]);
+
+ }
+
+ // Notify the listeners
+ for (IClientChangeListener listener : listenersCopy) {
+ // we attempt to catch any exception so that a bad listener doesn't kill our
+ // thread
+ try {
+ listener.clientChanged(client, changeMask);
+ } catch (Exception e) {
+ Log.e(DDMS, e);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link DeviceMonitor} object.
+ */
+ DeviceMonitor getDeviceMonitor() {
+ return mDeviceMonitor;
+ }
+
+ /**
+ * Starts the adb host side server.
+ * @return true if success
+ */
+ synchronized boolean startAdb() {
+ if (mAdbOsLocation == null) {
+ Log.e(ADB,
+ "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+ return false;
+ }
+
+ Process proc;
+ int status = -1;
+
+ try {
+ String[] command = new String[2];
+ command[0] = mAdbOsLocation;
+ command[1] = "start-server"; //$NON-NLS-1$
+ Log.d(DDMS,
+ String.format("Launching '%1$s %2$s' to ensure ADB is running.", //$NON-NLS-1$
+ mAdbOsLocation, command[1]));
+ proc = Runtime.getRuntime().exec(command);
+
+ ArrayList<String> errorOutput = new ArrayList<String>();
+ ArrayList<String> stdOutput = new ArrayList<String>();
+ status = grabProcessOutput(proc, errorOutput, stdOutput,
+ false /* waitForReaders */);
+
+ } catch (IOException ioe) {
+ Log.d(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$
+ // we'll return false;
+ } catch (InterruptedException ie) {
+ Log.d(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$
+ // we'll return false;
+ }
+
+ if (status != 0) {
+ Log.w(DDMS,
+ "'adb start-server' failed -- run manually if necessary"); //$NON-NLS-1$
+ return false;
+ }
+
+ Log.d(DDMS, "'adb start-server' succeeded"); //$NON-NLS-1$
+
+ return true;
+ }
+
+ /**
+ * Stops the adb host side server.
+ * @return true if success
+ */
+ private synchronized boolean stopAdb() {
+ if (mAdbOsLocation == null) {
+ Log.e(ADB,
+ "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+ return false;
+ }
+
+ Process proc;
+ int status = -1;
+
+ try {
+ String[] command = new String[2];
+ command[0] = mAdbOsLocation;
+ command[1] = "kill-server"; //$NON-NLS-1$
+ proc = Runtime.getRuntime().exec(command);
+ status = proc.waitFor();
+ }
+ catch (IOException ioe) {
+ // we'll return false;
+ }
+ catch (InterruptedException ie) {
+ // we'll return false;
+ }
+
+ if (status != 0) {
+ Log.w(DDMS,
+ "'adb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$
+ return false;
+ }
+
+ Log.d(DDMS, "'adb kill-server' succeeded"); //$NON-NLS-1$
+ return true;
+ }
+
+ /**
+ * Get the stderr/stdout outputs of a process and return when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the ouput from
+ * @param errorOutput The array to store the stderr output. cannot be null.
+ * @param stdOutput The array to store the stdout output. cannot be null.
+ * @param displayStdOut If true this will display stdout as well
+ * @param waitforReaders if true, this will wait for the reader threads.
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
+ final ArrayList<String> stdOutput, boolean waitforReaders)
+ throws InterruptedException {
+ assert errorOutput != null;
+ assert stdOutput != null;
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ Thread t1 = new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ Log.e(ADB, line);
+ errorOutput.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ Thread t2 = new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ Log.d(ADB, line);
+ stdOutput.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ t1.start();
+ t2.start();
+
+ // it looks like on windows process#waitFor() can return
+ // before the thread have filled the arrays, so we wait for both threads and the
+ // process itself.
+ if (waitforReaders) {
+ try {
+ t1.join();
+ } catch (InterruptedException e) {
+ }
+ try {
+ t2.join();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ // get the return code from the process
+ return process.waitFor();
+ }
+
+ /**
+ * Returns the singleton lock used by this class to protect any access to the listener.
+ * <p/>
+ * This includes adding/removing listeners, but also notifying listeners of new bridges,
+ * devices, and clients.
+ */
+ static Object getLock() {
+ return sLock;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
new file mode 100644
index 0000000..129b312
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
@@ -0,0 +1,35 @@
+/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package com.android.ddmlib;
+
+/**
+ * Thrown if the contents of a packet are bad.
+ */
+@SuppressWarnings("serial")
+class BadPacketException extends RuntimeException {
+ public BadPacketException()
+ {
+ super();
+ }
+
+ public BadPacketException(String msg)
+ {
+ super(msg);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java
new file mode 100644
index 0000000..441b024
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Subclass this with a class that handles one or more chunk types.
+ */
+abstract class ChunkHandler {
+
+ public static final int CHUNK_HEADER_LEN = 8; // 4-byte type, 4-byte len
+ public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN;
+
+ public static final int CHUNK_FAIL = type("FAIL");
+
+ ChunkHandler() {}
+
+ /**
+ * Client is ready. The monitor thread calls this method on all
+ * handlers when the client is determined to be DDM-aware (usually
+ * after receiving a HELO response.)
+ *
+ * The handler can use this opportunity to initialize client-side
+ * activity. Because there's a fair chance we'll want to send a
+ * message to the client, this method can throw an IOException.
+ */
+ abstract void clientReady(Client client) throws IOException;
+
+ /**
+ * Client has gone away. Can be used to clean up any resources
+ * associated with this client connection.
+ */
+ abstract void clientDisconnected(Client client);
+
+ /**
+ * Handle an incoming chunk. The data, of chunk type "type", begins
+ * at the start of "data" and continues to data.limit().
+ *
+ * If "isReply" is set, then "msgId" will be the ID of the request
+ * we sent to the client. Otherwise, it's the ID generated by the
+ * client for this event. Note that it's possible to receive chunks
+ * in reply packets for which we are not registered.
+ *
+ * The handler may not modify the contents of "data".
+ */
+ abstract void handleChunk(Client client, int type,
+ ByteBuffer data, boolean isReply, int msgId);
+
+ /**
+ * Handle chunks not recognized by handlers. The handleChunk() method
+ * in sub-classes should call this if the chunk type isn't recognized.
+ */
+ protected void handleUnknownChunk(Client client, int type,
+ ByteBuffer data, boolean isReply, int msgId) {
+ if (type == CHUNK_FAIL) {
+ int errorCode, msgLen;
+ String msg;
+
+ errorCode = data.getInt();
+ msgLen = data.getInt();
+ msg = getString(data, msgLen);
+ Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg);
+ } else {
+ Log.w("ddms", "WARNING: received unknown chunk " + name(type)
+ + ": len=" + data.limit() + ", reply=" + isReply
+ + ", msgId=0x" + Integer.toHexString(msgId));
+ }
+ Log.w("ddms", " client " + client + ", handler " + this);
+ }
+
+
+ /**
+ * Utility function to copy a String out of a ByteBuffer.
+ *
+ * This is here because multiple chunk handlers can make use of it,
+ * and there's nowhere better to put it.
+ */
+ static String getString(ByteBuffer buf, int len) {
+ char[] data = new char[len];
+ for (int i = 0; i < len; i++)
+ data[i] = buf.getChar();
+ return new String(data);
+ }
+
+ /**
+ * Utility function to copy a String into a ByteBuffer.
+ */
+ static void putString(ByteBuffer buf, String str) {
+ int len = str.length();
+ for (int i = 0; i < len; i++)
+ buf.putChar(str.charAt(i));
+ }
+
+ /**
+ * Convert a 4-character string to a 32-bit type.
+ */
+ static int type(String typeName) {
+ int val = 0;
+
+ if (typeName.length() != 4) {
+ Log.e("ddms", "Type name must be 4 letter long");
+ throw new RuntimeException("Type name must be 4 letter long");
+ }
+
+ for (int i = 0; i < 4; i++) {
+ val <<= 8;
+ val |= (byte) typeName.charAt(i);
+ }
+
+ return val;
+ }
+
+ /**
+ * Convert an integer type to a 4-character string.
+ */
+ static String name(int type) {
+ char[] ascii = new char[4];
+
+ ascii[0] = (char) ((type >> 24) & 0xff);
+ ascii[1] = (char) ((type >> 16) & 0xff);
+ ascii[2] = (char) ((type >> 8) & 0xff);
+ ascii[3] = (char) (type & 0xff);
+
+ return new String(ascii);
+ }
+
+ /**
+ * Allocate a ByteBuffer with enough space to hold the JDWP packet
+ * header and one chunk header in addition to the demands of the
+ * chunk being created.
+ *
+ * "maxChunkLen" indicates the size of the chunk contents only.
+ */
+ static ByteBuffer allocBuffer(int maxChunkLen) {
+ ByteBuffer buf =
+ ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen);
+ buf.order(CHUNK_ORDER);
+ return buf;
+ }
+
+ /**
+ * Return the slice of the JDWP packet buffer that holds just the
+ * chunk data.
+ */
+ static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) {
+ ByteBuffer slice;
+
+ assert jdwpBuf.position() == 0;
+
+ jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN);
+ slice = jdwpBuf.slice();
+ slice.order(CHUNK_ORDER);
+ jdwpBuf.position(0);
+
+ return slice;
+ }
+
+ /**
+ * Write the chunk header at the start of the chunk.
+ *
+ * Pass in the byte buffer returned by JdwpPacket.getPayload().
+ */
+ static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) {
+ ByteBuffer buf = packet.getPayload();
+
+ buf.putInt(0x00, type);
+ buf.putInt(0x04, chunkLen);
+
+ packet.finishPacket(CHUNK_HEADER_LEN + chunkLen);
+ }
+
+ /**
+ * Check that the client is opened with the proper debugger port for the
+ * specified application name, and if not, reopen it.
+ * @param client
+ * @param uiThread
+ * @param appName
+ * @return
+ */
+ protected static Client checkDebuggerPortForAppName(Client client, String appName) {
+ IDebugPortProvider provider = DebugPortManager.getProvider();
+ if (provider != null) {
+ Device device = client.getDevice();
+ int newPort = provider.getPort(device, appName);
+
+ if (newPort != IDebugPortProvider.NO_STATIC_PORT &&
+ newPort != client.getDebuggerListenPort()) {
+
+ AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+ if (bridge != null) {
+ DeviceMonitor deviceMonitor = bridge.getDeviceMonitor();
+ if (deviceMonitor != null) {
+ deviceMonitor.addClientToDropAndReopen(client, newPort);
+ client = null;
+ }
+ }
+ }
+ }
+
+ return client;
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java
new file mode 100644
index 0000000..866d578
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java
@@ -0,0 +1,768 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.HashMap;
+
+/**
+ * This represents a single client, usually a DAlvik VM process.
+ * <p/>This class gives access to basic client information, as well as methods to perform actions
+ * on the client.
+ * <p/>More detailed information, usually updated in real time, can be access through the
+ * {@link ClientData} class. Each <code>Client</code> object has its own <code>ClientData</code>
+ * accessed through {@link #getClientData()}.
+ */
+public class Client {
+
+ private static final int SERVER_PROTOCOL_VERSION = 1;
+
+ /** Client change bit mask: application name change */
+ public static final int CHANGE_NAME = 0x0001;
+ /** Client change bit mask: debugger interest change */
+ public static final int CHANGE_DEBUGGER_INTEREST = 0x0002;
+ /** Client change bit mask: debugger port change */
+ public static final int CHANGE_PORT = 0x0004;
+ /** Client change bit mask: thread update flag change */
+ public static final int CHANGE_THREAD_MODE = 0x0008;
+ /** Client change bit mask: thread data updated */
+ public static final int CHANGE_THREAD_DATA = 0x0010;
+ /** Client change bit mask: heap update flag change */
+ public static final int CHANGE_HEAP_MODE = 0x0020;
+ /** Client change bit mask: head data updated */
+ public static final int CHANGE_HEAP_DATA = 0x0040;
+ /** Client change bit mask: native heap data updated */
+ public static final int CHANGE_NATIVE_HEAP_DATA = 0x0080;
+ /** Client change bit mask: thread stack trace updated */
+ public static final int CHANGE_THREAD_STACKTRACE = 0x0100;
+ /** Client change bit mask: allocation information updated */
+ public static final int CHANGE_HEAP_ALLOCATIONS = 0x0200;
+ /** Client change bit mask: allocation information updated */
+ public static final int CHANGE_HEAP_ALLOCATION_STATUS = 0x0400;
+
+ /** Client change bit mask: combination of {@link Client#CHANGE_NAME},
+ * {@link Client#CHANGE_DEBUGGER_INTEREST}, and {@link Client#CHANGE_PORT}.
+ */
+ public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_INTEREST | CHANGE_PORT;
+
+ private SocketChannel mChan;
+
+ // debugger we're associated with, if any
+ private Debugger mDebugger;
+ private int mDebuggerListenPort;
+
+ // list of IDs for requests we have sent to the client
+ private HashMap<Integer,ChunkHandler> mOutstandingReqs;
+
+ // chunk handlers stash state data in here
+ private ClientData mClientData;
+
+ // User interface state. Changing the value causes a message to be
+ // sent to the client.
+ private boolean mThreadUpdateEnabled;
+ private boolean mHeapUpdateEnabled;
+
+ /*
+ * Read/write buffers. We can get large quantities of data from the
+ * client, e.g. the response to a "give me the list of all known classes"
+ * request from the debugger. Requests from the debugger, and from us,
+ * are much smaller.
+ *
+ * Pass-through debugger traffic is sent without copying. "mWriteBuffer"
+ * is only used for data generated within Client.
+ */
+ private static final int INITIAL_BUF_SIZE = 2*1024;
+ private static final int MAX_BUF_SIZE = 200*1024*1024;
+ private ByteBuffer mReadBuffer;
+
+ private static final int WRITE_BUF_SIZE = 256;
+ private ByteBuffer mWriteBuffer;
+
+ private Device mDevice;
+
+ private int mConnState;
+
+ private static final int ST_INIT = 1;
+ private static final int ST_NOT_JDWP = 2;
+ private static final int ST_AWAIT_SHAKE = 10;
+ private static final int ST_NEED_DDM_PKT = 11;
+ private static final int ST_NOT_DDM = 12;
+ private static final int ST_READY = 13;
+ private static final int ST_ERROR = 20;
+ private static final int ST_DISCONNECTED = 21;
+
+
+ /**
+ * Create an object for a new client connection.
+ *
+ * @param device the device this client belongs to
+ * @param chan the connected {@link SocketChannel}.
+ * @param pid the client pid.
+ */
+ Client(Device device, SocketChannel chan, int pid) {
+ mDevice = device;
+ mChan = chan;
+
+ mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
+ mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE);
+
+ mOutstandingReqs = new HashMap<Integer,ChunkHandler>();
+
+ mConnState = ST_INIT;
+
+ mClientData = new ClientData(pid);
+
+ mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate();
+ mHeapUpdateEnabled = DdmPreferences.getInitialHeapUpdate();
+ }
+
+ /**
+ * Returns a string representation of the {@link Client} object.
+ */
+ @Override
+ public String toString() {
+ return "[Client pid: " + mClientData.getPid() + "]";
+ }
+
+ /**
+ * Returns the {@link Device} on which this Client is running.
+ */
+ public Device getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the debugger port for this client.
+ */
+ public int getDebuggerListenPort() {
+ return mDebuggerListenPort;
+ }
+
+ /**
+ * Returns <code>true</code> if the client VM is DDM-aware.
+ *
+ * Calling here is only allowed after the connection has been
+ * established.
+ */
+ public boolean isDdmAware() {
+ switch (mConnState) {
+ case ST_INIT:
+ case ST_NOT_JDWP:
+ case ST_AWAIT_SHAKE:
+ case ST_NEED_DDM_PKT:
+ case ST_NOT_DDM:
+ case ST_ERROR:
+ case ST_DISCONNECTED:
+ return false;
+ case ST_READY:
+ return true;
+ default:
+ assert false;
+ return false;
+ }
+ }
+
+ /**
+ * Returns <code>true</code> if a debugger is currently attached to the client.
+ */
+ public boolean isDebuggerAttached() {
+ return mDebugger.isDebuggerAttached();
+ }
+
+ /**
+ * Return the Debugger object associated with this client.
+ */
+ Debugger getDebugger() {
+ return mDebugger;
+ }
+
+ /**
+ * Returns the {@link ClientData} object containing this client information.
+ */
+ public ClientData getClientData() {
+ return mClientData;
+ }
+
+ /**
+ * Forces the client to execute its garbage collector.
+ */
+ public void executeGarbageCollector() {
+ try {
+ HandleHeap.sendHPGC(this);
+ } catch (IOException ioe) {
+ Log.w("ddms", "Send of HPGC message failed");
+ // ignore
+ }
+ }
+
+ /**
+ * Enables or disables the thread update.
+ * <p/>If <code>true</code> the VM will be able to send thread information. Thread information
+ * must be requested with {@link #requestThreadUpdate()}.
+ * @param enabled the enable flag.
+ */
+ public void setThreadUpdateEnabled(boolean enabled) {
+ mThreadUpdateEnabled = enabled;
+ if (enabled == false) {
+ mClientData.clearThreads();
+ }
+
+ try {
+ HandleThread.sendTHEN(this, enabled);
+ } catch (IOException ioe) {
+ // ignore it here; client will clean up shortly
+ ioe.printStackTrace();
+ }
+
+ update(CHANGE_THREAD_MODE);
+ }
+
+ /**
+ * Returns whether the thread update is enabled.
+ */
+ public boolean isThreadUpdateEnabled() {
+ return mThreadUpdateEnabled;
+ }
+
+ /**
+ * Sends a thread update request. This is asynchronous.
+ * <p/>The thread info can be accessed by {@link ClientData#getThreads()}. The notification
+ * that the new data is available will be received through
+ * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+ * containing the mask {@link #CHANGE_THREAD_DATA}.
+ */
+ public void requestThreadUpdate() {
+ HandleThread.requestThreadUpdate(this);
+ }
+
+ /**
+ * Sends a thread stack trace update request. This is asynchronous.
+ * <p/>The thread info can be accessed by {@link ClientData#getThreads()} and
+ * {@link ThreadInfo#getStackTrace()}.
+ * <p/>The notification that the new data is available
+ * will be received through {@link IClientChangeListener#clientChanged(Client, int)}
+ * with a <code>changeMask</code> containing the mask {@link #CHANGE_THREAD_STACKTRACE}.
+ */
+ public void requestThreadStackTrace(int threadId) {
+ HandleThread.requestThreadStackCallRefresh(this, threadId);
+ }
+
+ /**
+ * Enables or disables the heap update.
+ * <p/>If <code>true</code>, any GC will cause the client to send its heap information.
+ * <p/>The heap information can be accessed by {@link ClientData#getVmHeapData()}.
+ * <p/>The notification that the new data is available
+ * will be received through {@link IClientChangeListener#clientChanged(Client, int)}
+ * with a <code>changeMask</code> containing the value {@link #CHANGE_HEAP_DATA}.
+ * @param enabled the enable flag
+ */
+ public void setHeapUpdateEnabled(boolean enabled) {
+ mHeapUpdateEnabled = enabled;
+
+ try {
+ HandleHeap.sendHPIF(this,
+ enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER);
+
+ HandleHeap.sendHPSG(this,
+ enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE,
+ HandleHeap.WHAT_MERGE);
+ } catch (IOException ioe) {
+ // ignore it here; client will clean up shortly
+ }
+
+ update(CHANGE_HEAP_MODE);
+ }
+
+ /**
+ * Returns whether the heap update is enabled.
+ * @see #setHeapUpdateEnabled(boolean)
+ */
+ public boolean isHeapUpdateEnabled() {
+ return mHeapUpdateEnabled;
+ }
+
+ /**
+ * Sends a native heap update request. this is asynchronous.
+ * <p/>The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}.
+ * The notification that the new data is available will be received through
+ * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+ * containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}.
+ */
+ public boolean requestNativeHeapInformation() {
+ try {
+ HandleNativeHeap.sendNHGT(this);
+ return true;
+ } catch (IOException e) {
+ Log.e("ddmlib", e);
+ }
+
+ return false;
+ }
+
+ /**
+ * Enables or disables the Allocation tracker for this client.
+ * <p/>If enabled, the VM will start tracking allocation informations. A call to
+ * {@link #requestAllocationDetails()} will make the VM sends the information about all the
+ * allocations that happened between the enabling and the request.
+ * @param enable
+ * @see #requestAllocationDetails()
+ */
+ public void enableAllocationTracker(boolean enable) {
+ try {
+ HandleHeap.sendREAE(this, enable);
+ } catch (IOException e) {
+ Log.e("ddmlib", e);
+ }
+ }
+
+ /**
+ * Sends a request to the VM to send the enable status of the allocation tracking.
+ * This is asynchronous.
+ * <p/>The allocation status can be accessed by {@link ClientData#getAllocationStatus()}.
+ * The notification that the new status is available will be received through
+ * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+ * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}.
+ */
+ public void requestAllocationStatus() {
+ try {
+ HandleHeap.sendREAQ(this);
+ } catch (IOException e) {
+ Log.e("ddmlib", e);
+ }
+ }
+
+ /**
+ * Sends a request to the VM to send the information about all the allocations that have
+ * happened since the call to {@link #enableAllocationTracker(boolean)} with <var>enable</var>
+ * set to <code>null</code>. This is asynchronous.
+ * <p/>The allocation information can be accessed by {@link ClientData#getAllocations()}.
+ * The notification that the new data is available will be received through
+ * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+ * containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}.
+ */
+ public void requestAllocationDetails() {
+ try {
+ HandleHeap.sendREAL(this);
+ } catch (IOException e) {
+ Log.e("ddmlib", e);
+ }
+ }
+
+ /**
+ * Sends a kill message to the VM.
+ */
+ public void kill() {
+ try {
+ HandleExit.sendEXIT(this, 1);
+ } catch (IOException ioe) {
+ Log.w("ddms", "Send of EXIT message failed");
+ // ignore
+ }
+ }
+
+ /**
+ * Registers the client with a Selector.
+ */
+ void register(Selector sel) throws IOException {
+ if (mChan != null) {
+ mChan.register(sel, SelectionKey.OP_READ, this);
+ }
+ }
+
+ /**
+ * Sets the client to accept debugger connection on the "selected debugger port".
+ *
+ * @see AndroidDebugBridge#setSelectedClient(Client)
+ * @see DdmPreferences#setSelectedDebugPort(int)
+ */
+ public void setAsSelectedClient() {
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (monitorThread != null) {
+ monitorThread.setSelectedClient(this);
+ }
+ }
+
+ /**
+ * Returns whether this client is the current selected client, accepting debugger connection
+ * on the "selected debugger port".
+ *
+ * @see #setAsSelectedClient()
+ * @see AndroidDebugBridge#setSelectedClient(Client)
+ * @see DdmPreferences#setSelectedDebugPort(int)
+ */
+ public boolean isSelectedClient() {
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (monitorThread != null) {
+ return monitorThread.getSelectedClient() == this;
+ }
+
+ return false;
+ }
+
+ /**
+ * Tell the client to open a server socket channel and listen for
+ * connections on the specified port.
+ */
+ void listenForDebugger(int listenPort) throws IOException {
+ mDebuggerListenPort = listenPort;
+ mDebugger = new Debugger(this, listenPort);
+ }
+
+ /**
+ * Initiate the JDWP handshake.
+ *
+ * On failure, closes the socket and returns false.
+ */
+ boolean sendHandshake() {
+ assert mWriteBuffer.position() == 0;
+
+ try {
+ // assume write buffer can hold 14 bytes
+ JdwpPacket.putHandshake(mWriteBuffer);
+ int expectedLen = mWriteBuffer.position();
+ mWriteBuffer.flip();
+ if (mChan.write(mWriteBuffer) != expectedLen)
+ throw new IOException("partial handshake write");
+ }
+ catch (IOException ioe) {
+ Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage());
+ mConnState = ST_ERROR;
+ close(true /* notify */);
+ return false;
+ }
+ finally {
+ mWriteBuffer.clear();
+ }
+
+ mConnState = ST_AWAIT_SHAKE;
+
+ return true;
+ }
+
+
+ /**
+ * Send a non-DDM packet to the client.
+ *
+ * Equivalent to sendAndConsume(packet, null).
+ */
+ void sendAndConsume(JdwpPacket packet) throws IOException {
+ sendAndConsume(packet, null);
+ }
+
+ /**
+ * Send a DDM packet to the client.
+ *
+ * Ideally, we can do this with a single channel write. If that doesn't
+ * happen, we have to prevent anybody else from writing to the channel
+ * until this packet completes, so we synchronize on the channel.
+ *
+ * Another goal is to avoid unnecessary buffer copies, so we write
+ * directly out of the JdwpPacket's ByteBuffer.
+ */
+ void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler)
+ throws IOException {
+
+ if (mChan == null) {
+ // can happen for e.g. THST packets
+ Log.v("ddms", "Not sending packet -- client is closed");
+ return;
+ }
+
+ if (replyHandler != null) {
+ /*
+ * Add the ID to the list of outstanding requests. We have to do
+ * this before sending the packet, in case the response comes back
+ * before our thread returns from the packet-send function.
+ */
+ addRequestId(packet.getId(), replyHandler);
+ }
+
+ synchronized (mChan) {
+ try {
+ packet.writeAndConsume(mChan);
+ }
+ catch (IOException ioe) {
+ removeRequestId(packet.getId());
+ throw ioe;
+ }
+ }
+ }
+
+ /**
+ * Forward the packet to the debugger (if still connected to one).
+ *
+ * Consumes the packet.
+ */
+ void forwardPacketToDebugger(JdwpPacket packet)
+ throws IOException {
+
+ Debugger dbg = mDebugger;
+
+ if (dbg == null) {
+ Log.i("ddms", "Discarding packet");
+ packet.consume();
+ } else {
+ dbg.sendAndConsume(packet);
+ }
+ }
+
+ /**
+ * Read data from our channel.
+ *
+ * This is called when data is known to be available, and we don't yet
+ * have a full packet in the buffer. If the buffer is at capacity,
+ * expand it.
+ */
+ void read()
+ throws IOException, BufferOverflowException {
+
+ int count;
+
+ if (mReadBuffer.position() == mReadBuffer.capacity()) {
+ if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
+ Log.e("ddms", "Exceeded MAX_BUF_SIZE!");
+ throw new BufferOverflowException();
+ }
+ Log.d("ddms", "Expanding read buffer to "
+ + mReadBuffer.capacity() * 2);
+
+ ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2);
+
+ // copy entire buffer to new buffer
+ mReadBuffer.position(0);
+ newBuffer.put(mReadBuffer); // leaves "position" at end of copied
+
+ mReadBuffer = newBuffer;
+ }
+
+ count = mChan.read(mReadBuffer);
+ if (count < 0)
+ throw new IOException("read failed");
+
+ if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this);
+ //Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(),
+ // mReadBuffer.arrayOffset(), mReadBuffer.position());
+ }
+
+ /**
+ * Return information for the first full JDWP packet in the buffer.
+ *
+ * If we don't yet have a full packet, return null.
+ *
+ * If we haven't yet received the JDWP handshake, we watch for it here
+ * and consume it without admitting to have done so. Upon receipt
+ * we send out the "HELO" message, which is why this can throw an
+ * IOException.
+ */
+ JdwpPacket getJdwpPacket() throws IOException {
+
+ /*
+ * On entry, the data starts at offset 0 and ends at "position".
+ * "limit" is set to the buffer capacity.
+ */
+ if (mConnState == ST_AWAIT_SHAKE) {
+ /*
+ * The first thing we get from the client is a response to our
+ * handshake. It doesn't look like a packet, so we have to
+ * handle it specially.
+ */
+ int result;
+
+ result = JdwpPacket.findHandshake(mReadBuffer);
+ //Log.v("ddms", "findHand: " + result);
+ switch (result) {
+ case JdwpPacket.HANDSHAKE_GOOD:
+ Log.i("ddms",
+ "Good handshake from client, sending HELO to " + mClientData.getPid());
+ JdwpPacket.consumeHandshake(mReadBuffer);
+ mConnState = ST_NEED_DDM_PKT;
+ HandleHello.sendHELO(this, SERVER_PROTOCOL_VERSION);
+ // see if we have another packet in the buffer
+ return getJdwpPacket();
+ case JdwpPacket.HANDSHAKE_BAD:
+ Log.i("ddms", "Bad handshake from client");
+ if (MonitorThread.getInstance().getRetryOnBadHandshake()) {
+ // we should drop the client, but also attempt to reopen it.
+ // This is done by the DeviceMonitor.
+ mDevice.getMonitor().addClientToDropAndReopen(this,
+ IDebugPortProvider.NO_STATIC_PORT);
+ } else {
+ // mark it as bad, close the socket, and don't retry
+ mConnState = ST_NOT_JDWP;
+ close(true /* notify */);
+ }
+ break;
+ case JdwpPacket.HANDSHAKE_NOTYET:
+ Log.i("ddms", "No handshake from client yet.");
+ break;
+ default:
+ Log.e("ddms", "Unknown packet while waiting for client handshake");
+ }
+ return null;
+ } else if (mConnState == ST_NEED_DDM_PKT ||
+ mConnState == ST_NOT_DDM ||
+ mConnState == ST_READY) {
+ /*
+ * Normal packet traffic.
+ */
+ if (mReadBuffer.position() != 0) {
+ if (Log.Config.LOGV) Log.v("ddms",
+ "Checking " + mReadBuffer.position() + " bytes");
+ }
+ return JdwpPacket.findPacket(mReadBuffer);
+ } else {
+ /*
+ * Not expecting data when in this state.
+ */
+ Log.e("ddms", "Receiving data in state = " + mConnState);
+ }
+
+ return null;
+ }
+
+ /*
+ * Add the specified ID to the list of request IDs for which we await
+ * a response.
+ */
+ private void addRequestId(int id, ChunkHandler handler) {
+ synchronized (mOutstandingReqs) {
+ if (Log.Config.LOGV) Log.v("ddms",
+ "Adding req 0x" + Integer.toHexString(id) +" to set");
+ mOutstandingReqs.put(id, handler);
+ }
+ }
+
+ /*
+ * Remove the specified ID from the list, if present.
+ */
+ void removeRequestId(int id) {
+ synchronized (mOutstandingReqs) {
+ if (Log.Config.LOGV) Log.v("ddms",
+ "Removing req 0x" + Integer.toHexString(id) + " from set");
+ mOutstandingReqs.remove(id);
+ }
+
+ //Log.w("ddms", "Request " + Integer.toHexString(id)
+ // + " could not be removed from " + this);
+ }
+
+ /**
+ * Determine whether this is a response to a request we sent earlier.
+ * If so, return the ChunkHandler responsible.
+ */
+ ChunkHandler isResponseToUs(int id) {
+
+ synchronized (mOutstandingReqs) {
+ ChunkHandler handler = mOutstandingReqs.get(id);
+ if (handler != null) {
+ if (Log.Config.LOGV) Log.v("ddms",
+ "Found 0x" + Integer.toHexString(id)
+ + " in request set - " + handler);
+ return handler;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * An earlier request resulted in a failure. This is the expected
+ * response to a HELO message when talking to a non-DDM client.
+ */
+ void packetFailed(JdwpPacket reply) {
+ if (mConnState == ST_NEED_DDM_PKT) {
+ Log.i("ddms", "Marking " + this + " as non-DDM client");
+ mConnState = ST_NOT_DDM;
+ } else if (mConnState != ST_NOT_DDM) {
+ Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req");
+ }
+ }
+
+ /**
+ * The MonitorThread calls this when it sees a DDM request or reply.
+ * If we haven't seen a DDM packet before, we advance the state to
+ * ST_READY and return "false". Otherwise, just return true.
+ *
+ * The idea is to let the MonitorThread know when we first see a DDM
+ * packet, so we can send a broadcast to the handlers when a client
+ * connection is made. This method is synchronized so that we only
+ * send the broadcast once.
+ */
+ synchronized boolean ddmSeen() {
+ if (mConnState == ST_NEED_DDM_PKT) {
+ mConnState = ST_READY;
+ return false;
+ } else if (mConnState != ST_READY) {
+ Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState);
+ }
+ return true;
+ }
+
+ /**
+ * Close the client socket channel. If there is a debugger associated
+ * with us, close that too.
+ *
+ * Closing a channel automatically unregisters it from the selector.
+ * However, we have to iterate through the selector loop before it
+ * actually lets them go and allows the file descriptors to close.
+ * The caller is expected to manage that.
+ * @param notify Whether or not to notify the listeners of a change.
+ */
+ void close(boolean notify) {
+ Log.i("ddms", "Closing " + this.toString());
+
+ mOutstandingReqs.clear();
+
+ try {
+ if (mChan != null) {
+ mChan.close();
+ mChan = null;
+ }
+
+ if (mDebugger != null) {
+ mDebugger.close();
+ mDebugger = null;
+ }
+ }
+ catch (IOException ioe) {
+ Log.w("ddms", "failed to close " + this);
+ // swallow it -- not much else to do
+ }
+
+ mDevice.removeClient(this, notify);
+ }
+
+ /**
+ * Returns whether this {@link Client} has a valid connection to the application VM.
+ */
+ public boolean isValid() {
+ return mChan != null;
+ }
+
+ void update(int changeMask) {
+ mDevice.update(this, changeMask);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java
new file mode 100644
index 0000000..2b46b6f
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+
+/**
+ * Contains the data of a {@link Client}.
+ */
+public class ClientData {
+ /* This is a place to stash data associated with a Client, such as thread
+ * states or heap data. ClientData maps 1:1 to Client, but it's a little
+ * cleaner if we separate the data out.
+ *
+ * Message handlers are welcome to stash arbitrary data here.
+ *
+ * IMPORTANT: The data here is written by HandleFoo methods and read by
+ * FooPanel methods, which run in different threads. All non-trivial
+ * access should be synchronized against the ClientData object.
+ */
+
+
+ /** Temporary name of VM to be ignored. */
+ private final static String PRE_INITIALIZED = "<pre-initialized>"; //$NON-NLS-1$
+
+ /** Debugger connection status: not waiting on one, not connected to one, but accepting
+ * new connections. This is the default value. */
+ public static final int DEBUGGER_DEFAULT = 1;
+ /**
+ * Debugger connection status: the application's VM is paused, waiting for a debugger to
+ * connect to it before resuming. */
+ public static final int DEBUGGER_WAITING = 2;
+ /** Debugger connection status : Debugger is connected */
+ public static final int DEBUGGER_ATTACHED = 3;
+ /** Debugger connection status: The listening port for debugger connection failed to listen.
+ * No debugger will be able to connect. */
+ public static final int DEBUGGER_ERROR = 4;
+
+ /**
+ * Allocation tracking status: unknown.
+ * <p/>This happens right after a {@link Client} is discovered
+ * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query regarding
+ * its allocation tracking status.
+ * @see Client#requestAllocationStatus()
+ */
+ public static final int ALLOCATION_TRACKING_UNKNOWN = -1;
+ /**
+ * Allocation tracking status: the {@link Client} is not tracking allocations. */
+ public static final int ALLOCATION_TRACKING_OFF = 0;
+ /**
+ * Allocation tracking status: the {@link Client} is tracking allocations. */
+ public static final int ALLOCATION_TRACKING_ON = 1;
+
+ /**
+ * Name of the value representing the max size of the heap, in the {@link Map} returned by
+ * {@link #getVmHeapInfo(int)}
+ */
+ public final static String HEAP_MAX_SIZE_BYTES = "maxSizeInBytes"; // $NON-NLS-1$
+ /**
+ * Name of the value representing the size of the heap, in the {@link Map} returned by
+ * {@link #getVmHeapInfo(int)}
+ */
+ public final static String HEAP_SIZE_BYTES = "sizeInBytes"; // $NON-NLS-1$
+ /**
+ * Name of the value representing the number of allocated bytes of the heap, in the
+ * {@link Map} returned by {@link #getVmHeapInfo(int)}
+ */
+ public final static String HEAP_BYTES_ALLOCATED = "bytesAllocated"; // $NON-NLS-1$
+ /**
+ * Name of the value representing the number of objects in the heap, in the {@link Map}
+ * returned by {@link #getVmHeapInfo(int)}
+ */
+ public final static String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; // $NON-NLS-1$
+
+ // is this a DDM-aware client?
+ private boolean mIsDdmAware;
+
+ // the client's process ID
+ private final int mPid;
+
+ // Java VM identification string
+ private String mVmIdentifier;
+
+ // client's self-description
+ private String mClientDescription;
+
+ // how interested are we in a debugger?
+ private int mDebuggerInterest;
+
+ // Thread tracking (THCR, THDE).
+ private TreeMap<Integer,ThreadInfo> mThreadMap;
+
+ /** VM Heap data */
+ private final HeapData mHeapData = new HeapData();
+ /** Native Heap data */
+ private final HeapData mNativeHeapData = new HeapData();
+
+ private HashMap<Integer, HashMap<String, Long>> mHeapInfoMap =
+ new HashMap<Integer, HashMap<String, Long>>();
+
+
+ /** library map info. Stored here since the backtrace data
+ * is computed on a need to display basis.
+ */
+ private ArrayList<NativeLibraryMapInfo> mNativeLibMapInfo =
+ new ArrayList<NativeLibraryMapInfo>();
+
+ /** Native Alloc info list */
+ private ArrayList<NativeAllocationInfo> mNativeAllocationList =
+ new ArrayList<NativeAllocationInfo>();
+ private int mNativeTotalMemory;
+
+ private AllocationInfo[] mAllocations;
+ private int mAllocationStatus = ALLOCATION_TRACKING_UNKNOWN;
+
+ /**
+ * Heap Information.
+ * <p/>The heap is composed of several {@link HeapSegment} objects.
+ * <p/>A call to {@link #isHeapDataComplete()} will indicate if the segments (available through
+ * {@link #getHeapSegments()}) represent the full heap.
+ */
+ public static class HeapData {
+ private TreeSet<HeapSegment> mHeapSegments = new TreeSet<HeapSegment>();
+ private boolean mHeapDataComplete = false;
+ private byte[] mProcessedHeapData;
+ private Map<Integer, ArrayList<HeapSegmentElement>> mProcessedHeapMap;
+
+ /**
+ * Abandon the current list of heap segments.
+ */
+ public synchronized void clearHeapData() {
+ /* Abandon the old segments instead of just calling .clear().
+ * This lets the user hold onto the old set if it wants to.
+ */
+ mHeapSegments = new TreeSet<HeapSegment>();
+ mHeapDataComplete = false;
+ }
+
+ /**
+ * Add raw HPSG chunk data to the list of heap segments.
+ *
+ * @param data The raw data from an HPSG chunk.
+ */
+ synchronized void addHeapData(ByteBuffer data) {
+ HeapSegment hs;
+
+ if (mHeapDataComplete) {
+ clearHeapData();
+ }
+
+ try {
+ hs = new HeapSegment(data);
+ } catch (BufferUnderflowException e) {
+ System.err.println("Discarding short HPSG data (length " + data.limit() + ")");
+ return;
+ }
+
+ mHeapSegments.add(hs);
+ }
+
+ /**
+ * Called when all heap data has arrived.
+ */
+ synchronized void sealHeapData() {
+ mHeapDataComplete = true;
+ }
+
+ /**
+ * Returns whether the heap data has been sealed.
+ */
+ public boolean isHeapDataComplete() {
+ return mHeapDataComplete;
+ }
+
+ /**
+ * Get the collected heap data, if sealed.
+ *
+ * @return The list of heap segments if the heap data has been sealed, or null if it hasn't.
+ */
+ public Collection<HeapSegment> getHeapSegments() {
+ if (isHeapDataComplete()) {
+ return mHeapSegments;
+ }
+ return null;
+ }
+
+ /**
+ * Sets the processed heap data.
+ *
+ * @param heapData The new heap data (can be null)
+ */
+ public void setProcessedHeapData(byte[] heapData) {
+ mProcessedHeapData = heapData;
+ }
+
+ /**
+ * Get the processed heap data, if present.
+ *
+ * @return the processed heap data, or null.
+ */
+ public byte[] getProcessedHeapData() {
+ return mProcessedHeapData;
+ }
+
+ public void setProcessedHeapMap(Map<Integer, ArrayList<HeapSegmentElement>> heapMap) {
+ mProcessedHeapMap = heapMap;
+ }
+
+ public Map<Integer, ArrayList<HeapSegmentElement>> getProcessedHeapMap() {
+ return mProcessedHeapMap;
+ }
+
+
+ }
+
+
+ /**
+ * Generic constructor.
+ */
+ ClientData(int pid) {
+ mPid = pid;
+
+ mDebuggerInterest = DEBUGGER_DEFAULT;
+ mThreadMap = new TreeMap<Integer,ThreadInfo>();
+ }
+
+ /**
+ * Returns whether the process is DDM-aware.
+ */
+ public boolean isDdmAware() {
+ return mIsDdmAware;
+ }
+
+ /**
+ * Sets DDM-aware status.
+ */
+ void isDdmAware(boolean aware) {
+ mIsDdmAware = aware;
+ }
+
+ /**
+ * Returns the process ID.
+ */
+ public int getPid() {
+ return mPid;
+ }
+
+ /**
+ * Returns the Client's VM identifier.
+ */
+ public String getVmIdentifier() {
+ return mVmIdentifier;
+ }
+
+ /**
+ * Sets VM identifier.
+ */
+ void setVmIdentifier(String ident) {
+ mVmIdentifier = ident;
+ }
+
+ /**
+ * Returns the client description.
+ * <p/>This is generally the name of the package defined in the
+ * <code>AndroidManifest.xml</code>.
+ *
+ * @return the client description or <code>null</code> if not the description was not yet
+ * sent by the client.
+ */
+ public String getClientDescription() {
+ return mClientDescription;
+ }
+
+ /**
+ * Sets client description.
+ *
+ * There may be a race between HELO and APNM. Rather than try
+ * to enforce ordering on the device, we just don't allow an empty
+ * name to replace a specified one.
+ */
+ void setClientDescription(String description) {
+ if (mClientDescription == null && description.length() > 0) {
+ /*
+ * The application VM is first named <pre-initialized> before being assigned
+ * its real name.
+ * Depending on the timing, we can get an APNM chunk setting this name before
+ * another one setting the final actual name. So if we get a SetClientDescription
+ * with this value we ignore it.
+ */
+ if (PRE_INITIALIZED.equals(description) == false) {
+ mClientDescription = description;
+ }
+ }
+ }
+
+ /**
+ * Returns the debugger connection status. Possible values are {@link #DEBUGGER_DEFAULT},
+ * {@link #DEBUGGER_WAITING}, {@link #DEBUGGER_ATTACHED}, and {@link #DEBUGGER_ERROR}.
+ */
+ public int getDebuggerConnectionStatus() {
+ return mDebuggerInterest;
+ }
+
+ /**
+ * Sets debugger connection status.
+ */
+ void setDebuggerConnectionStatus(int val) {
+ mDebuggerInterest = val;
+ }
+
+ /**
+ * Sets the current heap info values for the specified heap.
+ *
+ * @param heapId The heap whose info to update
+ * @param sizeInBytes The size of the heap, in bytes
+ * @param bytesAllocated The number of bytes currently allocated in the heap
+ * @param objectsAllocated The number of objects currently allocated in
+ * the heap
+ */
+ // TODO: keep track of timestamp, reason
+ synchronized void setHeapInfo(int heapId, long maxSizeInBytes,
+ long sizeInBytes, long bytesAllocated, long objectsAllocated) {
+ HashMap<String, Long> heapInfo = new HashMap<String, Long>();
+ heapInfo.put(HEAP_MAX_SIZE_BYTES, maxSizeInBytes);
+ heapInfo.put(HEAP_SIZE_BYTES, sizeInBytes);
+ heapInfo.put(HEAP_BYTES_ALLOCATED, bytesAllocated);
+ heapInfo.put(HEAP_OBJECTS_ALLOCATED, objectsAllocated);
+ mHeapInfoMap.put(heapId, heapInfo);
+ }
+
+ /**
+ * Returns the {@link HeapData} object for the VM.
+ */
+ public HeapData getVmHeapData() {
+ return mHeapData;
+ }
+
+ /**
+ * Returns the {@link HeapData} object for the native code.
+ */
+ HeapData getNativeHeapData() {
+ return mNativeHeapData;
+ }
+
+ /**
+ * Returns an iterator over the list of known VM heap ids.
+ * <p/>
+ * The caller must synchronize on the {@link ClientData} object while iterating.
+ *
+ * @return an iterator over the list of heap ids
+ */
+ public synchronized Iterator<Integer> getVmHeapIds() {
+ return mHeapInfoMap.keySet().iterator();
+ }
+
+ /**
+ * Returns the most-recent info values for the specified VM heap.
+ *
+ * @param heapId The heap whose info should be returned
+ * @return a map containing the info values for the specified heap.
+ * Returns <code>null</code> if the heap ID is unknown.
+ */
+ public synchronized Map<String, Long> getVmHeapInfo(int heapId) {
+ return mHeapInfoMap.get(heapId);
+ }
+
+ /**
+ * Adds a new thread to the list.
+ */
+ synchronized void addThread(int threadId, String threadName) {
+ ThreadInfo attr = new ThreadInfo(threadId, threadName);
+ mThreadMap.put(threadId, attr);
+ }
+
+ /**
+ * Removes a thread from the list.
+ */
+ synchronized void removeThread(int threadId) {
+ mThreadMap.remove(threadId);
+ }
+
+ /**
+ * Returns the list of threads as {@link ThreadInfo} objects.
+ * <p/>The list is empty until a thread update was requested with
+ * {@link Client#requestThreadUpdate()}.
+ */
+ public synchronized ThreadInfo[] getThreads() {
+ Collection<ThreadInfo> threads = mThreadMap.values();
+ return threads.toArray(new ThreadInfo[threads.size()]);
+ }
+
+ /**
+ * Returns the {@link ThreadInfo} by thread id.
+ */
+ synchronized ThreadInfo getThread(int threadId) {
+ return mThreadMap.get(threadId);
+ }
+
+ synchronized void clearThreads() {
+ mThreadMap.clear();
+ }
+
+ /**
+ * Returns the list of {@link NativeAllocationInfo}.
+ * @see Client#requestNativeHeapInformation()
+ */
+ public synchronized List<NativeAllocationInfo> getNativeAllocationList() {
+ return Collections.unmodifiableList(mNativeAllocationList);
+ }
+
+ /**
+ * adds a new {@link NativeAllocationInfo} to the {@link Client}
+ * @param allocInfo The {@link NativeAllocationInfo} to add.
+ */
+ synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) {
+ mNativeAllocationList.add(allocInfo);
+ }
+
+ /**
+ * Clear the current malloc info.
+ */
+ synchronized void clearNativeAllocationInfo() {
+ mNativeAllocationList.clear();
+ }
+
+ /**
+ * Returns the total native memory.
+ * @see Client#requestNativeHeapInformation()
+ */
+ public synchronized int getTotalNativeMemory() {
+ return mNativeTotalMemory;
+ }
+
+ synchronized void setTotalNativeMemory(int totalMemory) {
+ mNativeTotalMemory = totalMemory;
+ }
+
+ synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) {
+ mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library));
+ }
+
+ /**
+ * Returns an {@link Iterator} on {@link NativeLibraryMapInfo} objects.
+ * <p/>
+ * The caller must synchronize on the {@link ClientData} object while iterating.
+ */
+ public synchronized Iterator<NativeLibraryMapInfo> getNativeLibraryMapInfo() {
+ return mNativeLibMapInfo.iterator();
+ }
+
+ synchronized void setAllocationStatus(boolean enabled) {
+ mAllocationStatus = enabled ? ALLOCATION_TRACKING_ON : ALLOCATION_TRACKING_OFF;
+ }
+
+ /**
+ * Returns the allocation tracking status.
+ * @see Client#requestAllocationStatus()
+ */
+ public synchronized int getAllocationStatus() {
+ return mAllocationStatus;
+ }
+
+ synchronized void setAllocations(AllocationInfo[] allocs) {
+ mAllocations = allocs;
+ }
+
+ /**
+ * Returns the list of tracked allocations.
+ * @see Client#requestAllocationDetails()
+ */
+ public synchronized AllocationInfo[] getAllocations() {
+ return mAllocations;
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java
new file mode 100644
index 0000000..c96d40d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+/**
+ * Preferences for the ddm library.
+ * <p/>This class does not handle storing the preferences. It is merely a central point for
+ * applications using the ddmlib to override the default values.
+ * <p/>Various components of the ddmlib query this class to get their values.
+ * <p/>Calls to some <code>set##()</code> methods will update the components using the values
+ * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)}
+ * has been called.
+ * <p/>Check the documentation of each method.
+ */
+public final class DdmPreferences {
+
+ /** Default value for thread update flag upon client connection. */
+ public final static boolean DEFAULT_INITIAL_THREAD_UPDATE = false;
+ /** Default value for heap update flag upon client connection. */
+ public final static boolean DEFAULT_INITIAL_HEAP_UPDATE = false;
+ /** Default value for the selected client debug port */
+ public final static int DEFAULT_SELECTED_DEBUG_PORT = 8700;
+ /** Default value for the debug port base */
+ public final static int DEFAULT_DEBUG_PORT_BASE = 8600;
+ /** Default value for the logcat {@link LogLevel} */
+ public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR;
+
+ private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE;
+ private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE;
+
+ private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT;
+ private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE;
+ private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL;
+
+ /**
+ * Returns the initial {@link Client} flag for thread updates.
+ * @see #setInitialThreadUpdate(boolean)
+ */
+ public static boolean getInitialThreadUpdate() {
+ return sThreadUpdate;
+ }
+
+ /**
+ * Sets the initial {@link Client} flag for thread updates.
+ * <p/>This change takes effect right away, for newly created {@link Client} objects.
+ */
+ public static void setInitialThreadUpdate(boolean state) {
+ sThreadUpdate = state;
+ }
+
+ /**
+ * Returns the initial {@link Client} flag for heap updates.
+ * @see #setInitialHeapUpdate(boolean)
+ */
+ public static boolean getInitialHeapUpdate() {
+ return sInitialHeapUpdate;
+ }
+
+ /**
+ * Sets the initial {@link Client} flag for heap updates.
+ * <p/>If <code>true</code>, the {@link ClientData} will automatically be updated with
+ * the VM heap information whenever a GC happens.
+ * <p/>This change takes effect right away, for newly created {@link Client} objects.
+ */
+ public static void setInitialHeapUpdate(boolean state) {
+ sInitialHeapUpdate = state;
+ }
+
+ /**
+ * Returns the debug port used by the selected {@link Client}.
+ */
+ public static int getSelectedDebugPort() {
+ return sSelectedDebugPort;
+ }
+
+ /**
+ * Sets the debug port used by the selected {@link Client}.
+ * <p/>This change takes effect right away.
+ * @param port the new port to use.
+ */
+ public static void setSelectedDebugPort(int port) {
+ sSelectedDebugPort = port;
+
+ MonitorThread monitorThread = MonitorThread.getInstance();
+ if (monitorThread != null) {
+ monitorThread.setDebugSelectedPort(port);
+ }
+ }
+
+ /**
+ * Returns the debug port used by the first {@link Client}. Following clients, will use the
+ * next port.
+ */
+ public static int getDebugPortBase() {
+ return sDebugPortBase;
+ }
+
+ /**
+ * Sets the debug port used by the first {@link Client}.
+ * <p/>Once a port is used, the next Client will use port + 1. Quitting applications will
+ * release their debug port, and new clients will be able to reuse them.
+ * <p/>This must be called before {@link AndroidDebugBridge#init(boolean)}.
+ */
+ public static void setDebugPortBase(int port) {
+ sDebugPortBase = port;
+ }
+
+ /**
+ * Returns the minimum {@link LogLevel} being displayed.
+ */
+ public static LogLevel getLogLevel() {
+ return sLogLevel;
+ }
+
+ /**
+ * Sets the minimum {@link LogLevel} to display.
+ * <p/>This change takes effect right away.
+ */
+ public static void setLogLevel(String value) {
+ sLogLevel = LogLevel.getByString(value);
+
+ Log.setLevel(sLogLevel);
+ }
+
+ /**
+ * Non accessible constructor.
+ */
+ private DdmPreferences() {
+ // pass, only static methods in the class.
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java
new file mode 100644
index 0000000..9392127
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Device;
+
+/**
+ * Centralized point to provide a {@link IDebugPortProvider} to ddmlib.
+ *
+ * <p/>When {@link Client} objects are created, they start listening for debuggers on a specific
+ * port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and
+ * increment this value for each new <code>Client</code>.
+ *
+ * <p/>This {@link DebugPortManager} allows applications using ddmlib to provide a custom
+ * port provider on a per-<code>Client</code> basis, depending on the device/emulator they are
+ * running on, and/or their names.
+ */
+public class DebugPortManager {
+
+ /**
+ * Classes which implement this interface provide a method that provides a non random
+ * debugger port for a newly created {@link Client}.
+ */
+ public interface IDebugPortProvider {
+
+ public static final int NO_STATIC_PORT = -1;
+
+ /**
+ * Returns a non-random debugger port for the specified application running on the
+ * specified {@link Device}.
+ * @param device The device the application is running on.
+ * @param appName The application name, as defined in the <code>AndroidManifest.xml</code>
+ * <var>package</var> attribute of the <var>manifest</var> node.
+ * @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client}
+ * should use the automatic debugger port provider.
+ */
+ public int getPort(Device device, String appName);
+ }
+
+ private static IDebugPortProvider sProvider = null;
+
+ /**
+ * Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests
+ * a debugger port.
+ * @param provider the <code>IDebugPortProvider</code> to use.
+ */
+ public static void setProvider(IDebugPortProvider provider) {
+ sProvider = provider;
+ }
+
+ /**
+ * Returns the
+ * @return
+ */
+ static IDebugPortProvider getProvider() {
+ return sProvider;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java
new file mode 100644
index 0000000..f30509a
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+/**
+ * This represents a pending or established connection with a JDWP debugger.
+ */
+class Debugger {
+
+ /*
+ * Messages from the debugger should be pretty small; may not even
+ * need an expanding-buffer implementation for this.
+ */
+ private static final int INITIAL_BUF_SIZE = 1 * 1024;
+ private static final int MAX_BUF_SIZE = 32 * 1024;
+ private ByteBuffer mReadBuffer;
+
+ private static final int PRE_DATA_BUF_SIZE = 256;
+ private ByteBuffer mPreDataBuffer;
+
+ /* connection state */
+ private int mConnState;
+ private static final int ST_NOT_CONNECTED = 1;
+ private static final int ST_AWAIT_SHAKE = 2;
+ private static final int ST_READY = 3;
+
+ /* peer */
+ private Client mClient; // client we're forwarding to/from
+ private int mListenPort; // listen to me
+ private ServerSocketChannel mListenChannel;
+
+ /* this goes up and down; synchronize methods that access the field */
+ private SocketChannel mChannel;
+
+ /**
+ * Create a new Debugger object, configured to listen for connections
+ * on a specific port.
+ */
+ Debugger(Client client, int listenPort) throws IOException {
+
+ mClient = client;
+ mListenPort = listenPort;
+
+ mListenChannel = ServerSocketChannel.open();
+ mListenChannel.configureBlocking(false); // required for Selector
+
+ InetSocketAddress addr = new InetSocketAddress(
+ InetAddress.getByName("localhost"), // $NON-NLS-1$
+ listenPort);
+ mListenChannel.socket().setReuseAddress(true); // enable SO_REUSEADDR
+ mListenChannel.socket().bind(addr);
+
+ mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
+ mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE);
+ mConnState = ST_NOT_CONNECTED;
+
+ Log.i("ddms", "Created: " + this.toString());
+ }
+
+ /**
+ * Returns "true" if a debugger is currently attached to us.
+ */
+ boolean isDebuggerAttached() {
+ return mChannel != null;
+ }
+
+ /**
+ * Represent the Debugger as a string.
+ */
+ @Override
+ public String toString() {
+ // mChannel != null means we have connection, ST_READY means it's going
+ return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid()
+ + ((mConnState != ST_READY) ? " inactive]" : " active]");
+ }
+
+ /**
+ * Register the debugger's listen socket with the Selector.
+ */
+ void registerListener(Selector sel) throws IOException {
+ mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this);
+ }
+
+ /**
+ * Return the Client being debugged.
+ */
+ Client getClient() {
+ return mClient;
+ }
+
+ /**
+ * Accept a new connection, but only if we don't already have one.
+ *
+ * Must be synchronized with other uses of mChannel and mPreBuffer.
+ *
+ * Returns "null" if we're already talking to somebody.
+ */
+ synchronized SocketChannel accept() throws IOException {
+ return accept(mListenChannel);
+ }
+
+ /**
+ * Accept a new connection from the specified listen channel. This
+ * is so we can listen on a dedicated port for the "current" client,
+ * where "current" is constantly in flux.
+ *
+ * Must be synchronized with other uses of mChannel and mPreBuffer.
+ *
+ * Returns "null" if we're already talking to somebody.
+ */
+ synchronized SocketChannel accept(ServerSocketChannel listenChan)
+ throws IOException {
+
+ if (listenChan != null) {
+ SocketChannel newChan;
+
+ newChan = listenChan.accept();
+ if (mChannel != null) {
+ Log.w("ddms", "debugger already talking to " + mClient
+ + " on " + mListenPort);
+ newChan.close();
+ return null;
+ }
+ mChannel = newChan;
+ mChannel.configureBlocking(false); // required for Selector
+ mConnState = ST_AWAIT_SHAKE;
+ return mChannel;
+ }
+
+ return null;
+ }
+
+ /**
+ * Close the data connection only.
+ */
+ synchronized void closeData() {
+ try {
+ if (mChannel != null) {
+ mChannel.close();
+ mChannel = null;
+ mConnState = ST_NOT_CONNECTED;
+
+ ClientData cd = mClient.getClientData();
+ cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_DEFAULT);
+ mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
+ }
+ } catch (IOException ioe) {
+ Log.w("ddms", "Failed to close data " + this);
+ }
+ }
+
+ /**
+ * Close the socket that's listening for new connections and (if
+ * we're connected) the debugger data socket.
+ */
+ synchronized void close() {
+ try {
+ if (mListenChannel != null) {
+ mListenChannel.close();
+ }
+ mListenChannel = null;
+ closeData();
+ } catch (IOException ioe) {
+ Log.w("ddms", "Failed to close listener " + this);
+ }
+ }
+
+ // TODO: ?? add a finalizer that verifies the channel was closed
+
+ /**
+ * Read data from our channel.
+ *
+ * This is called when data is known to be available, and we don't yet
+ * have a full packet in the buffer. If the buffer is at capacity,
+ * expand it.
+ */
+ void read() throws IOException {
+ int count;
+
+ if (mReadBuffer.position() == mReadBuffer.capacity()) {
+ if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
+ throw new BufferOverflowException();
+ }
+ Log.d("ddms", "Expanding read buffer to "
+ + mReadBuffer.capacity() * 2);
+
+ ByteBuffer newBuffer =
+ ByteBuffer.allocate(mReadBuffer.capacity() * 2);
+ mReadBuffer.position(0);
+ newBuffer.put(mReadBuffer); // leaves "position" at end
+
+ mReadBuffer = newBuffer;
+ }
+
+ count = mChannel.read(mReadBuffer);
+ Log.v("ddms", "Read " + count + " bytes from " + this);
+ if (count < 0) throw new IOException("read failed");
+ }
+
+ /**
+ * Return information for the first full JDWP packet in the buffer.
+ *
+ * If we don't yet have a full packet, return null.
+ *
+ * If we haven't yet received the JDWP handshake, we watch for it here
+ * and consume it without admitting to have done so. We also send
+ * the handshake response to the debugger, along with any pending
+ * pre-connection data, which is why this can throw an IOException.
+ */
+ JdwpPacket getJdwpPacket() throws IOException {
+ /*
+ * On entry, the data starts at offset 0 and ends at "position".
+ * "limit" is set to the buffer capacity.
+ */
+ if (mConnState == ST_AWAIT_SHAKE) {
+ int result;
+
+ result = JdwpPacket.findHandshake(mReadBuffer);
+ //Log.v("ddms", "findHand: " + result);
+ switch (result) {
+ case JdwpPacket.HANDSHAKE_GOOD:
+ Log.i("ddms", "Good handshake from debugger");
+ JdwpPacket.consumeHandshake(mReadBuffer);
+ sendHandshake();
+ mConnState = ST_READY;
+
+ ClientData cd = mClient.getClientData();
+ cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_ATTACHED);
+ mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
+
+ // see if we have another packet in the buffer
+ return getJdwpPacket();
+ case JdwpPacket.HANDSHAKE_BAD:
+ // not a debugger, throw an exception so we drop the line
+ Log.i("ddms", "Bad handshake from debugger");
+ throw new IOException("bad handshake");
+ case JdwpPacket.HANDSHAKE_NOTYET:
+ break;
+ default:
+ Log.e("ddms", "Unknown packet while waiting for client handshake");
+ }
+ return null;
+ } else if (mConnState == ST_READY) {
+ if (mReadBuffer.position() != 0) {
+ Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes");
+ }
+ return JdwpPacket.findPacket(mReadBuffer);
+ } else {
+ Log.e("ddms", "Receiving data in state = " + mConnState);
+ }
+
+ return null;
+ }
+
+ /**
+ * Forward a packet to the client.
+ *
+ * "mClient" will never be null, though it's possible that the channel
+ * in the client has closed and our send attempt will fail.
+ *
+ * Consumes the packet.
+ */
+ void forwardPacketToClient(JdwpPacket packet) throws IOException {
+ mClient.sendAndConsume(packet);
+ }
+
+ /**
+ * Send the handshake to the debugger. We also send along any packets
+ * we already received from the client (usually just a VM_START event,
+ * if anything at all).
+ */
+ private synchronized void sendHandshake() throws IOException {
+ ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN);
+ JdwpPacket.putHandshake(tempBuffer);
+ int expectedLength = tempBuffer.position();
+ tempBuffer.flip();
+ if (mChannel.write(tempBuffer) != expectedLength) {
+ throw new IOException("partial handshake write");
+ }
+
+ expectedLength = mPreDataBuffer.position();
+ if (expectedLength > 0) {
+ Log.d("ddms", "Sending " + mPreDataBuffer.position()
+ + " bytes of saved data");
+ mPreDataBuffer.flip();
+ if (mChannel.write(mPreDataBuffer) != expectedLength) {
+ throw new IOException("partial pre-data write");
+ }
+ mPreDataBuffer.clear();
+ }
+ }
+
+ /**
+ * Send a packet to the debugger.
+ *
+ * Ideally, we can do this with a single channel write. If that doesn't
+ * happen, we have to prevent anybody else from writing to the channel
+ * until this packet completes, so we synchronize on the channel.
+ *
+ * Another goal is to avoid unnecessary buffer copies, so we write
+ * directly out of the JdwpPacket's ByteBuffer.
+ *
+ * We must synchronize on "mChannel" before writing to it. We want to
+ * coordinate the buffered data with mChannel creation, so this whole
+ * method is synchronized.
+ */
+ synchronized void sendAndConsume(JdwpPacket packet)
+ throws IOException {
+
+ if (mChannel == null) {
+ /*
+ * Buffer this up so we can send it to the debugger when it
+ * finally does connect. This is essential because the VM_START
+ * message might be telling the debugger that the VM is
+ * suspended. The alternative approach would be for us to
+ * capture and interpret VM_START and send it later if we
+ * didn't choose to un-suspend the VM for our own purposes.
+ */
+ Log.d("ddms", "Saving packet 0x"
+ + Integer.toHexString(packet.getId()));
+ packet.movePacket(mPreDataBuffer);
+ } else {
+ packet.writeAndConsume(mChannel);
+ }
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java
new file mode 100644
index 0000000..0e7f0bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A Device. It can be a physical device or an emulator.
+ *
+ * TODO: make this class package-protected, and shift all callers to use IDevice
+ */
+public final class Device implements IDevice {
+ /**
+ * The state of a device.
+ */
+ public static enum DeviceState {
+ BOOTLOADER("bootloader"), //$NON-NLS-1$
+ OFFLINE("offline"), //$NON-NLS-1$
+ ONLINE("device"); //$NON-NLS-1$
+
+ private String mState;
+
+ DeviceState(String state) {
+ mState = state;
+ }
+
+ /**
+ * Returns a {@link DeviceState} from the string returned by <code>adb devices</code>.
+ * @param state the device state.
+ * @return a {@link DeviceState} object or <code>null</code> if the state is unknown.
+ */
+ public static DeviceState getState(String state) {
+ for (DeviceState deviceState : values()) {
+ if (deviceState.mState.equals(state)) {
+ return deviceState;
+ }
+ }
+ return null;
+ }
+ }
+
+ /** Emulator Serial Number regexp. */
+ final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$
+
+ /** Serial number of the device */
+ String serialNumber = null;
+
+ /** Name of the AVD */
+ String mAvdName = null;
+
+ /** State of the device. */
+ DeviceState state = null;
+
+ /** Device properties. */
+ private final Map<String, String> mProperties = new HashMap<String, String>();
+
+ private final ArrayList<Client> mClients = new ArrayList<Client>();
+ private DeviceMonitor mMonitor;
+
+ /**
+ * Socket for the connection monitoring client connection/disconnection.
+ */
+ private SocketChannel mSocketChannel;
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getSerialNumber()
+ */
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public String getAvdName() {
+ return mAvdName;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getState()
+ */
+ public DeviceState getState() {
+ return state;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getProperties()
+ */
+ public Map<String, String> getProperties() {
+ return Collections.unmodifiableMap(mProperties);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getPropertyCount()
+ */
+ public int getPropertyCount() {
+ return mProperties.size();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getProperty(java.lang.String)
+ */
+ public String getProperty(String name) {
+ return mProperties.get(name);
+ }
+
+
+ @Override
+ public String toString() {
+ return serialNumber;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isOnline()
+ */
+ public boolean isOnline() {
+ return state == DeviceState.ONLINE;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isEmulator()
+ */
+ public boolean isEmulator() {
+ return serialNumber.matches(RE_EMULATOR_SN);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isOffline()
+ */
+ public boolean isOffline() {
+ return state == DeviceState.OFFLINE;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#isBootLoader()
+ */
+ public boolean isBootLoader() {
+ return state == DeviceState.BOOTLOADER;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#hasClients()
+ */
+ public boolean hasClients() {
+ return mClients.size() > 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClients()
+ */
+ public Client[] getClients() {
+ synchronized (mClients) {
+ return mClients.toArray(new Client[mClients.size()]);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClient(java.lang.String)
+ */
+ public Client getClient(String applicationName) {
+ synchronized (mClients) {
+ for (Client c : mClients) {
+ if (applicationName.equals(c.getClientData().getClientDescription())) {
+ return c;
+ }
+ }
+
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getSyncService()
+ */
+ public SyncService getSyncService() {
+ SyncService syncService = new SyncService(AndroidDebugBridge.sSocketAddr, this);
+ if (syncService.openSync()) {
+ return syncService;
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getFileListingService()
+ */
+ public FileListingService getFileListingService() {
+ return new FileListingService(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getScreenshot()
+ */
+ public RawImage getScreenshot() throws IOException {
+ return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver)
+ */
+ public void executeShellCommand(String command, IShellOutputReceiver receiver)
+ throws IOException {
+ AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this,
+ receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver)
+ */
+ public void runEventLogService(LogReceiver receiver) throws IOException {
+ AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver)
+ */
+ public void runLogService(String logname,
+ LogReceiver receiver) throws IOException {
+ AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#createForward(int, int)
+ */
+ public boolean createForward(int localPort, int remotePort) {
+ try {
+ return AdbHelper.createForward(AndroidDebugBridge.sSocketAddr, this,
+ localPort, remotePort);
+ } catch (IOException e) {
+ Log.e("adb-forward", e); //$NON-NLS-1$
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#removeForward(int, int)
+ */
+ public boolean removeForward(int localPort, int remotePort) {
+ try {
+ return AdbHelper.removeForward(AndroidDebugBridge.sSocketAddr, this,
+ localPort, remotePort);
+ } catch (IOException e) {
+ Log.e("adb-remove-forward", e); //$NON-NLS-1$
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IDevice#getClientName(int)
+ */
+ public String getClientName(int pid) {
+ synchronized (mClients) {
+ for (Client c : mClients) {
+ if (c.getClientData().getPid() == pid) {
+ return c.getClientData().getClientDescription();
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ Device(DeviceMonitor monitor) {
+ mMonitor = monitor;
+ }
+
+ DeviceMonitor getMonitor() {
+ return mMonitor;
+ }
+
+ void addClient(Client client) {
+ synchronized (mClients) {
+ mClients.add(client);
+ }
+ }
+
+ List<Client> getClientList() {
+ return mClients;
+ }
+
+ boolean hasClient(int pid) {
+ synchronized (mClients) {
+ for (Client client : mClients) {
+ if (client.getClientData().getPid() == pid) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ void clearClientList() {
+ synchronized (mClients) {
+ mClients.clear();
+ }
+ }
+
+ /**
+ * Sets the client monitoring socket.
+ * @param socketChannel the sockets
+ */
+ void setClientMonitoringSocket(SocketChannel socketChannel) {
+ mSocketChannel = socketChannel;
+ }
+
+ /**
+ * Returns the client monitoring socket.
+ */
+ SocketChannel getClientMonitoringSocket() {
+ return mSocketChannel;
+ }
+
+ /**
+ * Removes a {@link Client} from the list.
+ * @param client the client to remove.
+ * @param notify Whether or not to notify the listeners of a change.
+ */
+ void removeClient(Client client, boolean notify) {
+ mMonitor.addPortToAvailableList(client.getDebuggerListenPort());
+ synchronized (mClients) {
+ mClients.remove(client);
+ }
+ if (notify) {
+ mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST);
+ }
+ }
+
+ void update(int changeMask) {
+ mMonitor.getServer().deviceChanged(this, changeMask);
+ }
+
+ void update(Client client, int changeMask) {
+ mMonitor.getServer().clientChanged(client, changeMask);
+ }
+
+ void addProperty(String label, String value) {
+ mProperties.put(label, value);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java
new file mode 100644
index 0000000..f9d0fa0
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java
@@ -0,0 +1,866 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.AdbHelper.AdbResponse;
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.Device.DeviceState;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A Device monitor. This connects to the Android Debug Bridge and get device and
+ * debuggable process information from it.
+ */
+final class DeviceMonitor {
+ private byte[] mLengthBuffer = new byte[4];
+ private byte[] mLengthBuffer2 = new byte[4];
+
+ private boolean mQuit = false;
+
+ private AndroidDebugBridge mServer;
+
+ private SocketChannel mMainAdbConnection = null;
+ private boolean mMonitoring = false;
+ private int mConnectionAttempt = 0;
+ private int mRestartAttemptCount = 0;
+ private boolean mInitialDeviceListDone = false;
+
+ private Selector mSelector;
+
+ private final ArrayList<Device> mDevices = new ArrayList<Device>();
+
+ private final ArrayList<Integer> mDebuggerPorts = new ArrayList<Integer>();
+
+ private final HashMap<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>();
+
+ /**
+ * Creates a new {@link DeviceMonitor} object and links it to the running
+ * {@link AndroidDebugBridge} object.
+ * @param server the running {@link AndroidDebugBridge}.
+ */
+ DeviceMonitor(AndroidDebugBridge server) {
+ mServer = server;
+
+ mDebuggerPorts.add(DdmPreferences.getDebugPortBase());
+ }
+
+ /**
+ * Starts the monitoring.
+ */
+ void start() {
+ new Thread("Device List Monitor") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ deviceMonitorLoop();
+ }
+ }.start();
+ }
+
+ /**
+ * Stops the monitoring.
+ */
+ void stop() {
+ mQuit = true;
+
+ // wakeup the main loop thread by closing the main connection to adb.
+ try {
+ if (mMainAdbConnection != null) {
+ mMainAdbConnection.close();
+ }
+ } catch (IOException e1) {
+ }
+
+ // wake up the secondary loop by closing the selector.
+ if (mSelector != null) {
+ mSelector.wakeup();
+ }
+ }
+
+
+
+ /**
+ * Returns if the monitor is currently connected to the debug bridge server.
+ * @return
+ */
+ boolean isMonitoring() {
+ return mMonitoring;
+ }
+
+ int getConnectionAttemptCount() {
+ return mConnectionAttempt;
+ }
+
+ int getRestartAttemptCount() {
+ return mRestartAttemptCount;
+ }
+
+ /**
+ * Returns the devices.
+ */
+ Device[] getDevices() {
+ synchronized (mDevices) {
+ return mDevices.toArray(new Device[mDevices.size()]);
+ }
+ }
+
+ boolean hasInitialDeviceList() {
+ return mInitialDeviceListDone;
+ }
+
+ AndroidDebugBridge getServer() {
+ return mServer;
+ }
+
+ void addClientToDropAndReopen(Client client, int port) {
+ synchronized (mClientsToReopen) {
+ Log.d("DeviceMonitor",
+ "Adding " + client + " to list of client to reopen (" + port +").");
+ if (mClientsToReopen.get(client) == null) {
+ mClientsToReopen.put(client, port);
+ }
+ }
+ mSelector.wakeup();
+ }
+
+ /**
+ * Monitors the devices. This connects to the Debug Bridge
+ */
+ private void deviceMonitorLoop() {
+ do {
+ try {
+ if (mMainAdbConnection == null) {
+ Log.d("DeviceMonitor", "Opening adb connection");
+ mMainAdbConnection = openAdbConnection();
+ if (mMainAdbConnection == null) {
+ mConnectionAttempt++;
+ Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt);
+ if (mConnectionAttempt > 10) {
+ if (mServer.startAdb() == false) {
+ mRestartAttemptCount++;
+ Log.e("DeviceMonitor",
+ "adb restart attempts: " + mRestartAttemptCount);
+ } else {
+ mRestartAttemptCount = 0;
+ }
+ }
+ waitABit();
+ } else {
+ Log.d("DeviceMonitor", "Connected to adb for device monitoring");
+ mConnectionAttempt = 0;
+ }
+ }
+
+ if (mMainAdbConnection != null && mMonitoring == false) {
+ mMonitoring = sendDeviceListMonitoringRequest();
+ }
+
+ if (mMonitoring) {
+ // read the length of the incoming message
+ int length = readLength(mMainAdbConnection, mLengthBuffer);
+
+ if (length >= 0) {
+ // read the incoming message
+ processIncomingDeviceData(length);
+
+ // flag the fact that we have build the list at least once.
+ mInitialDeviceListDone = true;
+ }
+ }
+ } catch (AsynchronousCloseException ace) {
+ // this happens because of a call to Quit. We do nothing, and the loop will break.
+ } catch (IOException ioe) {
+ if (mQuit == false) {
+ Log.e("DeviceMonitor", "Adb connection Error:" + ioe.getMessage());
+ mMonitoring = false;
+ if (mMainAdbConnection != null) {
+ try {
+ mMainAdbConnection.close();
+ } catch (IOException ioe2) {
+ // we can safely ignore that one.
+ }
+ mMainAdbConnection = null;
+ }
+ }
+ }
+ } while (mQuit == false);
+ }
+
+ /**
+ * Sleeps for a little bit.
+ */
+ private void waitABit() {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e1) {
+ }
+ }
+
+ /**
+ * Attempts to connect to the debug bridge server.
+ * @return a connect socket if success, null otherwise
+ */
+ private SocketChannel openAdbConnection() {
+ Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring...");
+
+ SocketChannel adbChannel = null;
+ try {
+ adbChannel = SocketChannel.open(AndroidDebugBridge.sSocketAddr);
+ adbChannel.socket().setTcpNoDelay(true);
+ } catch (IOException e) {
+ }
+
+ return adbChannel;
+ }
+
+ /**
+ *
+ * @return
+ * @throws IOException
+ */
+ private boolean sendDeviceListMonitoringRequest() throws IOException {
+ byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$
+
+ if (AdbHelper.write(mMainAdbConnection, request) == false) {
+ Log.e("DeviceMonitor", "Sending Tracking request failed!");
+ mMainAdbConnection.close();
+ throw new IOException("Sending Tracking request failed!");
+ }
+
+ AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection,
+ false /* readDiagString */);
+
+ if (resp.ioSuccess == false) {
+ Log.e("DeviceMonitor", "Failed to read the adb response!");
+ mMainAdbConnection.close();
+ throw new IOException("Failed to read the adb response!");
+ }
+
+ if (resp.okay == false) {
+ // request was refused by adb!
+ Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+ }
+
+ return resp.okay;
+ }
+
+ /**
+ * Processes an incoming device message from the socket
+ * @param socket
+ * @param length
+ * @throws IOException
+ */
+ private void processIncomingDeviceData(int length) throws IOException {
+ ArrayList<Device> list = new ArrayList<Device>();
+
+ if (length > 0) {
+ byte[] buffer = new byte[length];
+ String result = read(mMainAdbConnection, buffer);
+
+ String[] devices = result.split("\n"); // $NON-NLS-1$
+
+ for (String d : devices) {
+ String[] param = d.split("\t"); // $NON-NLS-1$
+ if (param.length == 2) {
+ // new adb uses only serial numbers to identify devices
+ Device device = new Device(this);
+ device.serialNumber = param[0];
+ device.state = DeviceState.getState(param[1]);
+
+ //add the device to the list
+ list.add(device);
+ }
+ }
+ }
+
+ // now merge the new devices with the old ones.
+ updateDevices(list);
+ }
+
+ /**
+ * Updates the device list with the new items received from the monitoring service.
+ */
+ private void updateDevices(ArrayList<Device> newList) {
+ // because we are going to call mServer.deviceDisconnected which will acquire this lock
+ // we lock it first, so that the AndroidDebugBridge lock is always locked first.
+ synchronized (AndroidDebugBridge.getLock()) {
+ synchronized (mDevices) {
+ // For each device in the current list, we look for a matching the new list.
+ // * if we find it, we update the current object with whatever new information
+ // there is
+ // (mostly state change, if the device becomes ready, we query for build info).
+ // We also remove the device from the new list to mark it as "processed"
+ // * if we do not find it, we remove it from the current list.
+ // Once this is done, the new list contains device we aren't monitoring yet, so we
+ // add them to the list, and start monitoring them.
+
+ for (int d = 0 ; d < mDevices.size() ;) {
+ Device device = mDevices.get(d);
+
+ // look for a similar device in the new list.
+ int count = newList.size();
+ boolean foundMatch = false;
+ for (int dd = 0 ; dd < count ; dd++) {
+ Device newDevice = newList.get(dd);
+ // see if it matches in id and serial number.
+ if (newDevice.serialNumber.equals(device.serialNumber)) {
+ foundMatch = true;
+
+ // update the state if needed.
+ if (device.state != newDevice.state) {
+ device.state = newDevice.state;
+ device.update(Device.CHANGE_STATE);
+
+ // if the device just got ready/online, we need to start
+ // monitoring it.
+ if (device.isOnline()) {
+ if (AndroidDebugBridge.getClientSupport() == true) {
+ if (startMonitoringDevice(device) == false) {
+ Log.e("DeviceMonitor",
+ "Failed to start monitoring "
+ + device.serialNumber);
+ }
+ }
+
+ if (device.getPropertyCount() == 0) {
+ queryNewDeviceForInfo(device);
+ }
+ }
+ }
+
+ // remove the new device from the list since it's been used
+ newList.remove(dd);
+ break;
+ }
+ }
+
+ if (foundMatch == false) {
+ // the device is gone, we need to remove it, and keep current index
+ // to process the next one.
+ removeDevice(device);
+ mServer.deviceDisconnected(device);
+ } else {
+ // process the next one
+ d++;
+ }
+ }
+
+ // at this point we should still have some new devices in newList, so we
+ // process them.
+ for (Device newDevice : newList) {
+ // add them to the list
+ mDevices.add(newDevice);
+ mServer.deviceConnected(newDevice);
+
+ // start monitoring them.
+ if (AndroidDebugBridge.getClientSupport() == true) {
+ if (newDevice.isOnline()) {
+ startMonitoringDevice(newDevice);
+ }
+ }
+
+ // look for their build info.
+ if (newDevice.isOnline()) {
+ queryNewDeviceForInfo(newDevice);
+ }
+ }
+ }
+ }
+ newList.clear();
+ }
+
+ private void removeDevice(Device device) {
+ device.clearClientList();
+ mDevices.remove(device);
+
+ SocketChannel channel = device.getClientMonitoringSocket();
+ if (channel != null) {
+ try {
+ channel.close();
+ } catch (IOException e) {
+ // doesn't really matter if the close fails.
+ }
+ }
+ }
+
+ /**
+ * Queries a device for its build info.
+ * @param device the device to query.
+ */
+ private void queryNewDeviceForInfo(Device device) {
+ // TODO: do this in a separate thread.
+ try {
+ // first get the list of properties.
+ device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND,
+ new GetPropReceiver(device));
+
+ // now get the emulator Virtual Device name (if applicable).
+ if (device.isEmulator()) {
+ EmulatorConsole console = EmulatorConsole.getConsole(device);
+ if (console != null) {
+ device.mAvdName = console.getAvdName();
+ }
+ }
+ } catch (IOException e) {
+ // if we can't get the build info, it doesn't matter too much
+ }
+ }
+
+ /**
+ * Starts a monitoring service for a device.
+ * @param device the device to monitor.
+ * @return true if success.
+ */
+ private boolean startMonitoringDevice(Device device) {
+ SocketChannel socketChannel = openAdbConnection();
+
+ if (socketChannel != null) {
+ try {
+ boolean result = sendDeviceMonitoringRequest(socketChannel, device);
+ if (result) {
+
+ if (mSelector == null) {
+ startDeviceMonitorThread();
+ }
+
+ device.setClientMonitoringSocket(socketChannel);
+
+ synchronized (mDevices) {
+ // always wakeup before doing the register. The synchronized block
+ // ensure that the selector won't select() before the end of this block.
+ // @see deviceClientMonitorLoop
+ mSelector.wakeup();
+
+ socketChannel.configureBlocking(false);
+ socketChannel.register(mSelector, SelectionKey.OP_READ, device);
+ }
+
+ return true;
+ }
+ } catch (IOException e) {
+ try {
+ // attempt to close the socket if needed.
+ socketChannel.close();
+ } catch (IOException e1) {
+ // we can ignore that one. It may already have been closed.
+ }
+ Log.d("DeviceMonitor",
+ "Connection Failure when starting to monitor device '"
+ + device + "' : " + e.getMessage());
+ }
+ }
+
+ return false;
+ }
+
+ private void startDeviceMonitorThread() throws IOException {
+ mSelector = Selector.open();
+ new Thread("Device Client Monitor") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ deviceClientMonitorLoop();
+ }
+ }.start();
+ }
+
+ private void deviceClientMonitorLoop() {
+ do {
+ try {
+ // This synchronized block stops us from doing the select() if a new
+ // Device is being added.
+ // @see startMonitoringDevice()
+ synchronized (mDevices) {
+ }
+
+ int count = mSelector.select();
+
+ if (mQuit) {
+ return;
+ }
+
+ synchronized (mClientsToReopen) {
+ if (mClientsToReopen.size() > 0) {
+ Set<Client> clients = mClientsToReopen.keySet();
+ MonitorThread monitorThread = MonitorThread.getInstance();
+
+ for (Client client : clients) {
+ Device device = client.getDevice();
+ int pid = client.getClientData().getPid();
+
+ monitorThread.dropClient(client, false /* notify */);
+
+ // This is kinda bad, but if we don't wait a bit, the client
+ // will never answer the second handshake!
+ waitABit();
+
+ int port = mClientsToReopen.get(client);
+
+ if (port == IDebugPortProvider.NO_STATIC_PORT) {
+ port = getNextDebuggerPort();
+ }
+ Log.d("DeviceMonitor", "Reopening " + client);
+ openClient(device, pid, port, monitorThread);
+ device.update(Device.CHANGE_CLIENT_LIST);
+ }
+
+ mClientsToReopen.clear();
+ }
+ }
+
+ if (count == 0) {
+ continue;
+ }
+
+ Set<SelectionKey> keys = mSelector.selectedKeys();
+ Iterator<SelectionKey> iter = keys.iterator();
+
+ while (iter.hasNext()) {
+ SelectionKey key = iter.next();
+ iter.remove();
+
+ if (key.isValid() && key.isReadable()) {
+ Object attachment = key.attachment();
+
+ if (attachment instanceof Device) {
+ Device device = (Device)attachment;
+
+ SocketChannel socket = device.getClientMonitoringSocket();
+
+ if (socket != null) {
+ try {
+ int length = readLength(socket, mLengthBuffer2);
+
+ processIncomingJdwpData(device, socket, length);
+ } catch (IOException ioe) {
+ Log.d("DeviceMonitor",
+ "Error reading jdwp list: " + ioe.getMessage());
+ socket.close();
+
+ // restart the monitoring of that device
+ synchronized (mDevices) {
+ if (mDevices.contains(device)) {
+ Log.d("DeviceMonitor",
+ "Restarting monitoring service for " + device);
+ startMonitoringDevice(device);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (mQuit == false) {
+
+ }
+ }
+
+ } while (mQuit == false);
+ }
+
+ private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device)
+ throws IOException {
+
+ AdbHelper.setDevice(socket, device);
+
+ byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$
+
+ if (AdbHelper.write(socket, request) == false) {
+ Log.e("DeviceMonitor", "Sending jdwp tracking request failed!");
+ socket.close();
+ throw new IOException();
+ }
+
+ AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */);
+
+ if (resp.ioSuccess == false) {
+ Log.e("DeviceMonitor", "Failed to read the adb response!");
+ socket.close();
+ throw new IOException();
+ }
+
+ if (resp.okay == false) {
+ // request was refused by adb!
+ Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+ }
+
+ return resp.okay;
+ }
+
+ private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length)
+ throws IOException {
+ if (length >= 0) {
+ // array for the current pids.
+ ArrayList<Integer> pidList = new ArrayList<Integer>();
+
+ // get the string data if there are any
+ if (length > 0) {
+ byte[] buffer = new byte[length];
+ String result = read(monitorSocket, buffer);
+
+ // split each line in its own list and create an array of integer pid
+ String[] pids = result.split("\n"); //$NON-NLS-1$
+
+ for (String pid : pids) {
+ try {
+ pidList.add(Integer.valueOf(pid));
+ } catch (NumberFormatException nfe) {
+ // looks like this pid is not really a number. Lets ignore it.
+ continue;
+ }
+ }
+ }
+
+ MonitorThread monitorThread = MonitorThread.getInstance();
+
+ // Now we merge the current list with the old one.
+ // this is the same mechanism as the merging of the device list.
+
+ // For each client in the current list, we look for a matching the pid in the new list.
+ // * if we find it, we do nothing, except removing the pid from its list,
+ // to mark it as "processed"
+ // * if we do not find any match, we remove the client from the current list.
+ // Once this is done, the new list contains pids for which we don't have clients yet,
+ // so we create clients for them, add them to the list, and start monitoring them.
+
+ List<Client> clients = device.getClientList();
+
+ boolean changed = false;
+
+ // because MonitorThread#dropClient acquires first the monitorThread lock and then the
+ // Device client list lock (when removing the Client from the list), we have to make
+ // sure we acquire the locks in the same order, since another thread (MonitorThread),
+ // could call dropClient itself.
+ synchronized (monitorThread) {
+ synchronized (clients) {
+ for (int c = 0 ; c < clients.size() ;) {
+ Client client = clients.get(c);
+ int pid = client.getClientData().getPid();
+
+ // look for a matching pid
+ Integer match = null;
+ for (Integer matchingPid : pidList) {
+ if (pid == matchingPid.intValue()) {
+ match = matchingPid;
+ break;
+ }
+ }
+
+ if (match != null) {
+ pidList.remove(match);
+ c++; // move on to the next client.
+ } else {
+ // we need to drop the client. the client will remove itself from the
+ // list of its device which is 'clients', so there's no need to
+ // increment c.
+ // We ask the monitor thread to not send notification, as we'll do
+ // it once at the end.
+ monitorThread.dropClient(client, false /* notify */);
+ changed = true;
+ }
+ }
+ }
+ }
+
+ // at this point whatever pid is left in the list needs to be converted into Clients.
+ for (int newPid : pidList) {
+ openClient(device, newPid, getNextDebuggerPort(), monitorThread);
+ changed = true;
+ }
+
+ if (changed) {
+ mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST);
+ }
+ }
+ }
+
+ /**
+ * Opens and creates a new client.
+ * @return
+ */
+ private void openClient(Device device, int pid, int port, MonitorThread monitorThread) {
+
+ SocketChannel clientSocket;
+ try {
+ clientSocket = AdbHelper.createPassThroughConnection(
+ AndroidDebugBridge.sSocketAddr, device, pid);
+
+ // required for Selector
+ clientSocket.configureBlocking(false);
+ } catch (UnknownHostException uhe) {
+ Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid);
+ return;
+ } catch (IOException ioe) {
+ Log.w("DeviceMonitor",
+ "Failed to connect to client '" + pid + "': " + ioe.getMessage());
+ return ;
+ }
+
+ createClient(device, pid, clientSocket, port, monitorThread);
+ }
+
+ /**
+ * Creates a client and register it to the monitor thread
+ * @param device
+ * @param pid
+ * @param socket
+ * @param debuggerPort the debugger port.
+ * @param monitorThread the {@link MonitorThread} object.
+ */
+ private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort,
+ MonitorThread monitorThread) {
+
+ /*
+ * Successfully connected to something. Create a Client object, add
+ * it to the list, and initiate the JDWP handshake.
+ */
+
+ Client client = new Client(device, socket, pid);
+
+ if (client.sendHandshake()) {
+ try {
+ if (AndroidDebugBridge.getClientSupport()) {
+ client.listenForDebugger(debuggerPort);
+ }
+ client.requestAllocationStatus();
+ } catch (IOException ioe) {
+ client.getClientData().setDebuggerConnectionStatus(ClientData.DEBUGGER_ERROR);
+ Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger");
+ // oh well
+ }
+ } else {
+ Log.e("ddms", "Handshake with " + client + " failed!");
+ /*
+ * The handshake send failed. We could remove it now, but if the
+ * failure is "permanent" we'll just keep banging on it and
+ * getting the same result. Keep it in the list with its "error"
+ * state so we don't try to reopen it.
+ */
+ }
+
+ if (client.isValid()) {
+ device.addClient(client);
+ monitorThread.addClient(client);
+ } else {
+ client = null;
+ }
+ }
+
+ private int getNextDebuggerPort() {
+ // get the first port and remove it
+ synchronized (mDebuggerPorts) {
+ if (mDebuggerPorts.size() > 0) {
+ int port = mDebuggerPorts.get(0);
+
+ // remove it.
+ mDebuggerPorts.remove(0);
+
+ // if there's nothing left, add the next port to the list
+ if (mDebuggerPorts.size() == 0) {
+ mDebuggerPorts.add(port+1);
+ }
+
+ return port;
+ }
+ }
+
+ return -1;
+ }
+
+ void addPortToAvailableList(int port) {
+ if (port > 0) {
+ synchronized (mDebuggerPorts) {
+ // because there could be case where clients are closed twice, we have to make
+ // sure the port number is not already in the list.
+ if (mDebuggerPorts.indexOf(port) == -1) {
+ // add the port to the list while keeping it sorted. It's not like there's
+ // going to be tons of objects so we do it linearly.
+ int count = mDebuggerPorts.size();
+ for (int i = 0 ; i < count ; i++) {
+ if (port < mDebuggerPorts.get(i)) {
+ mDebuggerPorts.add(i, port);
+ break;
+ }
+ }
+ // TODO: check if we can compact the end of the list.
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the length of the next message from a socket.
+ * @param socket The {@link SocketChannel} to read from.
+ * @return the length, or 0 (zero) if no data is available from the socket.
+ * @throws IOException if the connection failed.
+ */
+ private int readLength(SocketChannel socket, byte[] buffer) throws IOException {
+ String msg = read(socket, buffer);
+
+ if (msg != null) {
+ try {
+ return Integer.parseInt(msg, 16);
+ } catch (NumberFormatException nfe) {
+ // we'll throw an exception below.
+ }
+ }
+
+ // we receive something we can't read. It's better to reset the connection at this point.
+ throw new IOException("Unable to read length");
+ }
+
+ /**
+ * Fills a buffer from a socket.
+ * @param socket
+ * @param buffer
+ * @return the content of the buffer as a string, or null if it failed to convert the buffer.
+ * @throws IOException
+ */
+ private String read(SocketChannel socket, byte[] buffer) throws IOException {
+ ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length);
+
+ while (buf.position() != buf.limit()) {
+ int count;
+
+ count = socket.read(buf);
+ if (count < 0) {
+ throw new IOException("EOF");
+ }
+ }
+
+ try {
+ return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ // we'll return null below.
+ }
+
+ return null;
+ }
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java
new file mode 100644
index 0000000..f3986ed
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.security.InvalidParameterException;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides control over emulated hardware of the Android emulator.
+ * <p/>This is basically a wrapper around the command line console normally used with telnet.
+ *<p/>
+ * Regarding line termination handling:<br>
+ * One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most
+ * implementations don't enforce it (the dos one does). In this particular case, this is mostly
+ * irrelevant since we don't use telnet in Java, but that means we want to make
+ * sure we use the same line termination than what the console expects. The console
+ * code removes <code>\r</code> and waits for <code>\n</code>.
+ * <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console.
+ * <p/>
+ * <b>This API will change in the near future.</b>
+ */
+public final class EmulatorConsole {
+
+ private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
+
+ private final static int WAIT_TIME = 5; // spin-wait sleep, in ms
+
+ private final static int STD_TIMEOUT = 5000; // standard delay, in ms
+
+ private final static String HOST = "127.0.0.1"; //$NON-NLS-1$
+
+ private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
+ private final static String COMMAND_GPS =
+ "geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$
+ "%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$
+ "1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$
+
+ private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
+
+ /**
+ * Array of delay values: no delay, gprs, edge/egprs, umts/3d
+ */
+ public final static int[] MIN_LATENCIES = new int[] {
+ 0, // No delay
+ 150, // gprs
+ 80, // edge/egprs
+ 35 // umts/3g
+ };
+
+ /**
+ * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa.
+ */
+ public final int[] DOWNLOAD_SPEEDS = new int[] {
+ 0, // full speed
+ 14400, // gsm
+ 43200, // hscsd
+ 80000, // gprs
+ 236800, // edge/egprs
+ 1920000, // umts/3g
+ 14400000 // hsdpa
+ };
+
+ /** Arrays of valid network speeds */
+ public final static String[] NETWORK_SPEEDS = new String[] {
+ "full", //$NON-NLS-1$
+ "gsm", //$NON-NLS-1$
+ "hscsd", //$NON-NLS-1$
+ "gprs", //$NON-NLS-1$
+ "edge", //$NON-NLS-1$
+ "umts", //$NON-NLS-1$
+ "hsdpa", //$NON-NLS-1$
+ };
+
+ /** Arrays of valid network latencies */
+ public final static String[] NETWORK_LATENCIES = new String[] {
+ "none", //$NON-NLS-1$
+ "gprs", //$NON-NLS-1$
+ "edge", //$NON-NLS-1$
+ "umts", //$NON-NLS-1$
+ };
+
+ /** Gsm Mode enum. */
+ public static enum GsmMode {
+ UNKNOWN((String)null),
+ UNREGISTERED(new String[] { "unregistered", "off" }),
+ HOME(new String[] { "home", "on" }),
+ ROAMING("roaming"),
+ SEARCHING("searching"),
+ DENIED("denied");
+
+ private final String[] tags;
+
+ GsmMode(String tag) {
+ if (tag != null) {
+ this.tags = new String[] { tag };
+ } else {
+ this.tags = new String[0];
+ }
+ }
+
+ GsmMode(String[] tags) {
+ this.tags = tags;
+ }
+
+ public static GsmMode getEnum(String tag) {
+ for (GsmMode mode : values()) {
+ for (String t : mode.tags) {
+ if (t.equals(tag)) {
+ return mode;
+ }
+ }
+ }
+ return UNKNOWN;
+ }
+
+ /**
+ * Returns the first tag of the enum.
+ */
+ public String getTag() {
+ if (tags.length > 0) {
+ return tags[0];
+ }
+ return null;
+ }
+ }
+
+ public final static String RESULT_OK = null;
+
+ private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
+ private final static Pattern sVoiceStatusRegexp = Pattern.compile(
+ "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sDataStatusRegexp = Pattern.compile(
+ "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sDownloadSpeedRegexp = Pattern.compile(
+ "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+ private final static Pattern sMinLatencyRegexp = Pattern.compile(
+ "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+ private final static HashMap<Integer, EmulatorConsole> sEmulators =
+ new HashMap<Integer, EmulatorConsole>();
+
+ /** Gsm Status class */
+ public static class GsmStatus {
+ /** Voice status. */
+ public GsmMode voice = GsmMode.UNKNOWN;
+ /** Data status. */
+ public GsmMode data = GsmMode.UNKNOWN;
+ }
+
+ /** Network Status class */
+ public static class NetworkStatus {
+ /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
+ public int speed = -1;
+ /** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */
+ public int latency = -1;
+ }
+
+ private int mPort;
+
+ private SocketChannel mSocketChannel;
+
+ private byte[] mBuffer = new byte[1024];
+
+ /**
+ * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can
+ * be an already existing console, or a new one if it hadn't been created yet.
+ * @param d The device that the console links to.
+ * @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed.
+ */
+ public static synchronized EmulatorConsole getConsole(Device d) {
+ // we need to make sure that the device is an emulator
+ Matcher m = sEmulatorRegexp.matcher(d.serialNumber);
+ if (m.matches()) {
+ // get the port number. This is the console port.
+ int port;
+ try {
+ port = Integer.parseInt(m.group(1));
+ if (port <= 0) {
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ // looks like we failed to get the port number. This is a bit strange since
+ // it's coming from a regexp that only accept digit, but we handle the case
+ // and return null.
+ return null;
+ }
+
+ EmulatorConsole console = sEmulators.get(port);
+
+ if (console != null) {
+ // if the console exist, we ping the emulator to check the connection.
+ if (console.ping() == false) {
+ RemoveConsole(console.mPort);
+ console = null;
+ }
+ }
+
+ if (console == null) {
+ // no console object exists for this port so we create one, and start
+ // the connection.
+ console = new EmulatorConsole(port);
+ if (console.start()) {
+ sEmulators.put(port, console);
+ } else {
+ console = null;
+ }
+ }
+
+ return console;
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes the console object associated with a port from the map.
+ * @param port The port of the console to remove.
+ */
+ private static synchronized void RemoveConsole(int port) {
+ sEmulators.remove(port);
+ }
+
+ private EmulatorConsole(int port) {
+ super();
+ mPort = port;
+ }
+
+ /**
+ * Starts the connection of the console.
+ * @return true if success.
+ */
+ private boolean start() {
+
+ InetSocketAddress socketAddr;
+ try {
+ InetAddress hostAddr = InetAddress.getByName(HOST);
+ socketAddr = new InetSocketAddress(hostAddr, mPort);
+ } catch (UnknownHostException e) {
+ return false;
+ }
+
+ try {
+ mSocketChannel = SocketChannel.open(socketAddr);
+ } catch (IOException e1) {
+ return false;
+ }
+
+ // read some stuff from it
+ readLines();
+
+ return true;
+ }
+
+ /**
+ * Ping the emulator to check if the connection is still alive.
+ * @return true if the connection is alive.
+ */
+ private synchronized boolean ping() {
+ // it looks like we can send stuff, even when the emulator quit, but we can't read
+ // from the socket. So we check the return of readLines()
+ if (sendCommand(COMMAND_PING)) {
+ return readLines() != null;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sends a KILL command to the emulator.
+ */
+ public synchronized void kill() {
+ if (sendCommand(COMMAND_KILL)) {
+ RemoveConsole(mPort);
+ }
+ }
+
+ public synchronized String getAvdName() {
+ if (sendCommand(COMMAND_AVD_NAME)) {
+ String[] result = readLines();
+ if (result != null && result.length == 2) { // this should be the name on first line,
+ // and ok on 2nd line
+ return result[0];
+ } else {
+ // try to see if there's a message after KO
+ Matcher m = RE_KO.matcher(result[result.length-1]);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the network status of the emulator.
+ * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
+ * <code>null</code> if the query failed.
+ */
+ public synchronized NetworkStatus getNetworkStatus() {
+ if (sendCommand(COMMAND_NETWORK_STATUS)) {
+ /* Result is in the format
+ Current network status:
+ download speed: 14400 bits/s (1.8 KB/s)
+ upload speed: 14400 bits/s (1.8 KB/s)
+ minimum latency: 0 ms
+ maximum latency: 0 ms
+ */
+ String[] result = readLines();
+
+ if (isValid(result)) {
+ // we only compare agains the min latency and the download speed
+ // let's not rely on the order of the output, and simply loop through
+ // the line testing the regexp.
+ NetworkStatus status = new NetworkStatus();
+ for (String line : result) {
+ Matcher m = sDownloadSpeedRegexp.matcher(line);
+ if (m.matches()) {
+ // get the string value
+ String value = m.group(1);
+
+ // get the index from the list
+ status.speed = getSpeedIndex(value);
+
+ // move on to next line.
+ continue;
+ }
+
+ m = sMinLatencyRegexp.matcher(line);
+ if (m.matches()) {
+ // get the string value
+ String value = m.group(1);
+
+ // get the index from the list
+ status.latency = getLatencyIndex(value);
+
+ // move on to next line.
+ continue;
+ }
+ }
+
+ return status;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current gsm status of the emulator
+ * @return a {@link GsmStatus} object containing the gms status, or <code>null</code>
+ * if the query failed.
+ */
+ public synchronized GsmStatus getGsmStatus() {
+ if (sendCommand(COMMAND_GSM_STATUS)) {
+ /*
+ * result is in the format:
+ * gsm status
+ * gsm voice state: home
+ * gsm data state: home
+ */
+
+ String[] result = readLines();
+ if (isValid(result)) {
+
+ GsmStatus status = new GsmStatus();
+
+ // let's not rely on the order of the output, and simply loop through
+ // the line testing the regexp.
+ for (String line : result) {
+ Matcher m = sVoiceStatusRegexp.matcher(line);
+ if (m.matches()) {
+ // get the string value
+ String value = m.group(1);
+
+ // get the index from the list
+ status.voice = GsmMode.getEnum(value.toLowerCase());
+
+ // move on to next line.
+ continue;
+ }
+
+ m = sDataStatusRegexp.matcher(line);
+ if (m.matches()) {
+ // get the string value
+ String value = m.group(1);
+
+ // get the index from the list
+ status.data = GsmMode.getEnum(value.toLowerCase());
+
+ // move on to next line.
+ continue;
+ }
+ }
+
+ return status;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the GSM voice mode.
+ * @param mode the {@link GsmMode} value.
+ * @return RESULT_OK if success, an error String otherwise.
+ * @throws InvalidParameterException if mode is an invalid value.
+ */
+ public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException {
+ if (mode == GsmMode.UNKNOWN) {
+ throw new InvalidParameterException();
+ }
+
+ String command = String.format(COMMAND_GSM_VOICE, mode.getTag());
+ return processCommand(command);
+ }
+
+ /**
+ * Sets the GSM data mode.
+ * @param mode the {@link GsmMode} value
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ * @throws InvalidParameterException if mode is an invalid value.
+ */
+ public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException {
+ if (mode == GsmMode.UNKNOWN) {
+ throw new InvalidParameterException();
+ }
+
+ String command = String.format(COMMAND_GSM_DATA, mode.getTag());
+ return processCommand(command);
+ }
+
+ /**
+ * Initiate an incoming call on the emulator.
+ * @param number a string representing the calling number.
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ */
+ public synchronized String call(String number) {
+ String command = String.format(COMMAND_GSM_CALL, number);
+ return processCommand(command);
+ }
+
+ /**
+ * Cancels a current call.
+ * @param number the number of the call to cancel
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ */
+ public synchronized String cancelCall(String number) {
+ String command = String.format(COMMAND_GSM_CANCEL_CALL, number);
+ return processCommand(command);
+ }
+
+ /**
+ * Sends an SMS to the emulator
+ * @param number The sender phone number
+ * @param message The SMS message. \ characters must be escaped. The carriage return is
+ * the 2 character sequence {'\', 'n' }
+ *
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ */
+ public synchronized String sendSms(String number, String message) {
+ String command = String.format(COMMAND_SMS_SEND, number, message);
+ return processCommand(command);
+ }
+
+ /**
+ * Sets the network speed.
+ * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table.
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ */
+ public synchronized String setNetworkSpeed(int selectionIndex) {
+ String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]);
+ return processCommand(command);
+ }
+
+ /**
+ * Sets the network latency.
+ * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table.
+ * @return {@link #RESULT_OK} if success, an error String otherwise.
+ */
+ public synchronized String setNetworkLatency(int selectionIndex) {
+ String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]);
+ return processCommand(command);
+ }
+
+ public synchronized String sendLocation(double longitude, double latitude, double elevation) {
+
+ Calendar c = Calendar.getInstance();
+
+ double absLong = Math.abs(longitude);
+ int longDegree = (int)Math.floor(absLong);
+ char longDirection = 'E';
+ if (longitude < 0) {
+ longDirection = 'W';
+ }
+
+ double longMinute = (absLong - Math.floor(absLong)) * 60;
+
+ double absLat = Math.abs(latitude);
+ int latDegree = (int)Math.floor(absLat);
+ char latDirection = 'N';
+ if (latitude < 0) {
+ latDirection = 'S';
+ }
+
+ double latMinute = (absLat - Math.floor(absLat)) * 60;
+
+ String command = String.format(COMMAND_GPS,
+ c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE),
+ c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND),
+ latDegree, latMinute, latDirection,
+ longDegree, longMinute, longDirection);
+
+ return processCommand(command);
+ }
+
+ /**
+ * Sends a command to the emulator console.
+ * @param command The command string. <b>MUST BE TERMINATED BY \n</b>.
+ * @return true if success
+ */
+ private boolean sendCommand(String command) {
+ boolean result = false;
+ try {
+ byte[] bCommand;
+ try {
+ bCommand = command.getBytes(DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ // wrong encoding...
+ return result;
+ }
+
+ // write the command
+ AdbHelper.write(mSocketChannel, bCommand, bCommand.length, AdbHelper.STD_TIMEOUT);
+
+ result = true;
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (result == false) {
+ // FIXME connection failed somehow, we need to disconnect the console.
+ RemoveConsole(mPort);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Sends a command to the emulator and parses its answer.
+ * @param command the command to send.
+ * @return {@link #RESULT_OK} if success, an error message otherwise.
+ */
+ private String processCommand(String command) {
+ if (sendCommand(command)) {
+ String[] result = readLines();
+
+ if (result != null && result.length > 0) {
+ Matcher m = RE_KO.matcher(result[result.length-1]);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ return RESULT_OK;
+ }
+
+ return "Unable to communicate with the emulator";
+ }
+
+ return "Unable to send command to the emulator";
+ }
+
+ /**
+ * Reads line from the console socket. This call is blocking until we read the lines:
+ * <ul>
+ * <li>OK\r\n</li>
+ * <li>KO<msg>\r\n</li>
+ * </ul>
+ * @return the array of strings read from the emulator.
+ */
+ private String[] readLines() {
+ try {
+ ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length);
+ int numWaits = 0;
+ boolean stop = false;
+
+ while (buf.position() != buf.limit() && stop == false) {
+ int count;
+
+ count = mSocketChannel.read(buf);
+ if (count < 0) {
+ return null;
+ } else if (count == 0) {
+ if (numWaits * WAIT_TIME > STD_TIMEOUT) {
+ return null;
+ }
+ // non-blocking spin
+ try {
+ Thread.sleep(WAIT_TIME);
+ } catch (InterruptedException ie) {
+ }
+ numWaits++;
+ } else {
+ numWaits = 0;
+ }
+
+ // check the last few char aren't OK. For a valid message to test
+ // we need at least 4 bytes (OK/KO + \r\n)
+ if (buf.position() >= 4) {
+ int pos = buf.position();
+ if (endsWithOK(pos) || lastLineIsKO(pos)) {
+ stop = true;
+ }
+ }
+ }
+
+ String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING);
+ return msg.split("\r\n"); //$NON-NLS-1$
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if the 4 characters *before* the current position are "OK\r\n"
+ * @param currentPosition The current position
+ */
+ private boolean endsWithOK(int currentPosition) {
+ if (mBuffer[currentPosition-1] == '\n' &&
+ mBuffer[currentPosition-2] == '\r' &&
+ mBuffer[currentPosition-3] == 'K' &&
+ mBuffer[currentPosition-4] == 'O') {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the last line starts with KO and is also terminated by \r\n
+ * @param currentPosition the current position
+ */
+ private boolean lastLineIsKO(int currentPosition) {
+ // first check that the last 2 characters are CRLF
+ if (mBuffer[currentPosition-1] != '\n' ||
+ mBuffer[currentPosition-2] != '\r') {
+ return false;
+ }
+
+ // now loop backward looking for the previous CRLF, or the beginning of the buffer
+ int i = 0;
+ for (i = currentPosition-3 ; i >= 0; i--) {
+ if (mBuffer[i] == '\n') {
+ // found \n!
+ if (i > 0 && mBuffer[i-1] == '\r') {
+ // found \r!
+ break;
+ }
+ }
+ }
+
+ // here it is either -1 if we reached the start of the buffer without finding
+ // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2
+ if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') {
+ // found error!
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the last line of the result does not start with KO
+ */
+ private boolean isValid(String[] result) {
+ if (result != null && result.length > 0) {
+ return !(RE_KO.matcher(result[result.length-1]).matches());
+ }
+ return false;
+ }
+
+ private int getLatencyIndex(String value) {
+ try {
+ // get the int value
+ int latency = Integer.parseInt(value);
+
+ // check for the speed from the index
+ for (int i = 0 ; i < MIN_LATENCIES.length; i++) {
+ if (MIN_LATENCIES[i] == latency) {
+ return i;
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Do nothing, we'll just return -1.
+ }
+
+ return -1;
+ }
+
+ private int getSpeedIndex(String value) {
+ try {
+ // get the int value
+ int speed = Integer.parseInt(value);
+
+ // check for the speed from the index
+ for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) {
+ if (DOWNLOAD_SPEEDS[i] == speed) {
+ return i;
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Do nothing, we'll just return -1.
+ }
+
+ return -1;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java
new file mode 100644
index 0000000..b50cf79
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java
@@ -0,0 +1,767 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides {@link Device} side file listing service.
+ * <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}.
+ */
+public final class FileListingService {
+
+ /** Pattern to find filenames that match "*.apk" */
+ private final static Pattern sApkPattern =
+ Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+ private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$
+
+ /** Pattern to parse the output of the 'pm -lf' command.<br>
+ * The output format looks like:<br>
+ * /data/app/myapp.apk=com.mypackage.myapp */
+ private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$
+
+ /** Top level data folder. */
+ public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$
+ /** Top level sdcard folder. */
+ public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$
+ /** Top level system folder. */
+ public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$
+ /** Top level temp folder. */
+ public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$
+ /** Application folder. */
+ public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$
+
+ private final static String[] sRootLevelApprovedItems = {
+ DIRECTORY_DATA,
+ DIRECTORY_SDCARD,
+ DIRECTORY_SYSTEM,
+ DIRECTORY_TEMP
+ };
+
+ public static final long REFRESH_RATE = 5000L;
+ /**
+ * Refresh test has to be slightly lower for precision issue.
+ */
+ static final long REFRESH_TEST = (long)(REFRESH_RATE * .8);
+
+ /** Entry type: File */
+ public static final int TYPE_FILE = 0;
+ /** Entry type: Directory */
+ public static final int TYPE_DIRECTORY = 1;
+ /** Entry type: Directory Link */
+ public static final int TYPE_DIRECTORY_LINK = 2;
+ /** Entry type: Block */
+ public static final int TYPE_BLOCK = 3;
+ /** Entry type: Character */
+ public static final int TYPE_CHARACTER = 4;
+ /** Entry type: Link */
+ public static final int TYPE_LINK = 5;
+ /** Entry type: Socket */
+ public static final int TYPE_SOCKET = 6;
+ /** Entry type: FIFO */
+ public static final int TYPE_FIFO = 7;
+ /** Entry type: Other */
+ public static final int TYPE_OTHER = 8;
+
+ /** Device side file separator. */
+ public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$
+
+ private static final String FILE_ROOT = "/"; //$NON-NLS-1$
+
+
+ /**
+ * Regexp pattern to parse the result from ls.
+ */
+ private static Pattern sLsPattern = Pattern.compile(
+ "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$
+
+ private Device mDevice;
+ private FileEntry mRoot;
+
+ private ArrayList<Thread> mThreadList = new ArrayList<Thread>();
+
+ /**
+ * Represents an entry in a directory. This can be a file or a directory.
+ */
+ public final static class FileEntry {
+ /** Pattern to escape filenames for shell command consumption. */
+ private final static Pattern sEscapePattern = Pattern.compile(
+ "([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$
+
+ /**
+ * Comparator object for FileEntry
+ */
+ private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() {
+ public int compare(FileEntry o1, FileEntry o2) {
+ if (o1 instanceof FileEntry && o2 instanceof FileEntry) {
+ FileEntry fe1 = (FileEntry)o1;
+ FileEntry fe2 = (FileEntry)o2;
+ return fe1.name.compareTo(fe2.name);
+ }
+ return 0;
+ }
+ };
+
+ FileEntry parent;
+ String name;
+ String info;
+ String permissions;
+ String size;
+ String date;
+ String time;
+ String owner;
+ String group;
+ int type;
+ boolean isAppPackage;
+
+ boolean isRoot;
+
+ /**
+ * Indicates whether the entry content has been fetched yet, or not.
+ */
+ long fetchTime = 0;
+
+ final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>();
+
+ /**
+ * Creates a new file entry.
+ * @param parent parent entry or null if entry is root
+ * @param name name of the entry.
+ * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE},
+ * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}.
+ */
+ private FileEntry(FileEntry parent, String name, int type, boolean isRoot) {
+ this.parent = parent;
+ this.name = name;
+ this.type = type;
+ this.isRoot = isRoot;
+
+ checkAppPackageStatus();
+ }
+
+ /**
+ * Returns the name of the entry
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the size string of the entry, as returned by <code>ls</code>.
+ */
+ public String getSize() {
+ return size;
+ }
+
+ /**
+ * Returns the size of the entry.
+ */
+ public int getSizeValue() {
+ return Integer.parseInt(size);
+ }
+
+ /**
+ * Returns the date string of the entry, as returned by <code>ls</code>.
+ */
+ public String getDate() {
+ return date;
+ }
+
+ /**
+ * Returns the time string of the entry, as returned by <code>ls</code>.
+ */
+ public String getTime() {
+ return time;
+ }
+
+ /**
+ * Returns the permission string of the entry, as returned by <code>ls</code>.
+ */
+ public String getPermissions() {
+ return permissions;
+ }
+
+ /**
+ * Returns the extra info for the entry.
+ * <p/>For a link, it will be a description of the link.
+ * <p/>For an application apk file it will be the application package as returned
+ * by the Package Manager.
+ */
+ public String getInfo() {
+ return info;
+ }
+
+ /**
+ * Return the full path of the entry.
+ * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator.
+ */
+ public String getFullPath() {
+ if (isRoot) {
+ return FILE_ROOT;
+ }
+ StringBuilder pathBuilder = new StringBuilder();
+ fillPathBuilder(pathBuilder, false);
+
+ return pathBuilder.toString();
+ }
+
+ /**
+ * Return the fully escaped path of the entry. This path is safe to use in a
+ * shell command line.
+ * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator
+ */
+ public String getFullEscapedPath() {
+ StringBuilder pathBuilder = new StringBuilder();
+ fillPathBuilder(pathBuilder, true);
+
+ return pathBuilder.toString();
+ }
+
+ /**
+ * Returns the path as a list of segments.
+ */
+ public String[] getPathSegments() {
+ ArrayList<String> list = new ArrayList<String>();
+ fillPathSegments(list);
+
+ return list.toArray(new String[list.size()]);
+ }
+
+ /**
+ * Returns true if the entry is a directory, false otherwise;
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Returns if the entry is a folder or a link to a folder.
+ */
+ public boolean isDirectory() {
+ return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK;
+ }
+
+ /**
+ * Returns the parent entry.
+ */
+ public FileEntry getParent() {
+ return parent;
+ }
+
+ /**
+ * Returns the cached children of the entry. This returns the cache created from calling
+ * <code>FileListingService.getChildren()</code>.
+ */
+ public FileEntry[] getCachedChildren() {
+ return mChildren.toArray(new FileEntry[mChildren.size()]);
+ }
+
+ /**
+ * Returns the child {@link FileEntry} matching the name.
+ * This uses the cached children list.
+ * @param name the name of the child to return.
+ * @return the FileEntry matching the name or null.
+ */
+ public FileEntry findChild(String name) {
+ for (FileEntry entry : mChildren) {
+ if (entry.name.equals(name)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the entry is the root.
+ */
+ public boolean isRoot() {
+ return isRoot;
+ }
+
+ void addChild(FileEntry child) {
+ mChildren.add(child);
+ }
+
+ void setChildren(ArrayList<FileEntry> newChildren) {
+ mChildren.clear();
+ mChildren.addAll(newChildren);
+ }
+
+ boolean needFetch() {
+ if (fetchTime == 0) {
+ return true;
+ }
+ long current = System.currentTimeMillis();
+ if (current-fetchTime > REFRESH_TEST) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns if the entry is a valid application package.
+ */
+ public boolean isApplicationPackage() {
+ return isAppPackage;
+ }
+
+ /**
+ * Returns if the file name is an application package name.
+ */
+ public boolean isAppFileName() {
+ Matcher m = sApkPattern.matcher(name);
+ return m.matches();
+ }
+
+ /**
+ * Recursively fills the pathBuilder with the full path
+ * @param pathBuilder a StringBuilder used to create the path.
+ * @param escapePath Whether the path need to be escaped for consumption by
+ * a shell command line.
+ */
+ protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) {
+ if (isRoot) {
+ return;
+ }
+
+ if (parent != null) {
+ parent.fillPathBuilder(pathBuilder, escapePath);
+ }
+ pathBuilder.append(FILE_SEPARATOR);
+ pathBuilder.append(escapePath ? escape(name) : name);
+ }
+
+ /**
+ * Recursively fills the segment list with the full path.
+ * @param list The list of segments to fill.
+ */
+ protected void fillPathSegments(ArrayList<String> list) {
+ if (isRoot) {
+ return;
+ }
+
+ if (parent != null) {
+ parent.fillPathSegments(list);
+ }
+
+ list.add(name);
+ }
+
+ /**
+ * Sets the internal app package status flag. This checks whether the entry is in an app
+ * directory like /data/app or /system/app
+ */
+ private void checkAppPackageStatus() {
+ isAppPackage = false;
+
+ String[] segments = getPathSegments();
+ if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) {
+ isAppPackage = DIRECTORY_APP.equals(segments[1]) &&
+ (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0]));
+ }
+ }
+
+ /**
+ * Returns an escaped version of the entry name.
+ * @param entryName
+ */
+ private String escape(String entryName) {
+ return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$
+ }
+ }
+
+ private class LsReceiver extends MultiLineReceiver {
+
+ private ArrayList<FileEntry> mEntryList;
+ private ArrayList<String> mLinkList;
+ private FileEntry[] mCurrentChildren;
+ private FileEntry mParentEntry;
+
+ /**
+ * Create an ls receiver/parser.
+ * @param currentChildren The list of current children. To prevent
+ * collapse during update, reusing the same FileEntry objects for
+ * files that were already there is paramount.
+ * @param entryList the list of new children to be filled by the
+ * receiver.
+ * @param linkList the list of link path to compute post ls, to figure
+ * out if the link pointed to a file or to a directory.
+ */
+ public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList,
+ ArrayList<String> linkList) {
+ mParentEntry = parentEntry;
+ mCurrentChildren = parentEntry.getCachedChildren();
+ mEntryList = entryList;
+ mLinkList = linkList;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ // no need to handle empty lines.
+ if (line.length() == 0) {
+ continue;
+ }
+
+ // run the line through the regexp
+ Matcher m = sLsPattern.matcher(line);
+ if (m.matches() == false) {
+ continue;
+ }
+
+ // get the name
+ String name = m.group(7);
+
+ // if the parent is root, we only accept selected items
+ if (mParentEntry.isRoot()) {
+ boolean found = false;
+ for (String approved : sRootLevelApprovedItems) {
+ if (approved.equals(name)) {
+ found = true;
+ break;
+ }
+ }
+
+ // if it's not in the approved list we skip this entry.
+ if (found == false) {
+ continue;
+ }
+ }
+
+ // get the rest of the groups
+ String permissions = m.group(1);
+ String owner = m.group(2);
+ String group = m.group(3);
+ String size = m.group(4);
+ String date = m.group(5);
+ String time = m.group(6);
+ String info = null;
+
+ // and the type
+ int objectType = TYPE_OTHER;
+ switch (permissions.charAt(0)) {
+ case '-' :
+ objectType = TYPE_FILE;
+ break;
+ case 'b' :
+ objectType = TYPE_BLOCK;
+ break;
+ case 'c' :
+ objectType = TYPE_CHARACTER;
+ break;
+ case 'd' :
+ objectType = TYPE_DIRECTORY;
+ break;
+ case 'l' :
+ objectType = TYPE_LINK;
+ break;
+ case 's' :
+ objectType = TYPE_SOCKET;
+ break;
+ case 'p' :
+ objectType = TYPE_FIFO;
+ break;
+ }
+
+
+ // now check what we may be linking to
+ if (objectType == TYPE_LINK) {
+ String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$
+
+ // we should have 2 segments
+ if (segments.length == 2) {
+ // update the entry name to not contain the link
+ name = segments[0];
+
+ // and the link name
+ info = segments[1];
+
+ // now get the path to the link
+ String[] pathSegments = info.split(FILE_SEPARATOR);
+ if (pathSegments.length == 1) {
+ // the link is to something in the same directory,
+ // unless the link is ..
+ if ("..".equals(pathSegments[0])) { //$NON-NLS-1$
+ // set the type and we're done.
+ objectType = TYPE_DIRECTORY_LINK;
+ } else {
+ // either we found the object already
+ // or we'll find it later.
+ }
+ }
+ }
+
+ // add an arrow in front to specify it's a link.
+ info = "-> " + info; //$NON-NLS-1$;
+ }
+
+ // get the entry, either from an existing one, or a new one
+ FileEntry entry = getExistingEntry(name);
+ if (entry == null) {
+ entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */);
+ }
+
+ // add some misc info
+ entry.permissions = permissions;
+ entry.size = size;
+ entry.date = date;
+ entry.time = time;
+ entry.owner = owner;
+ entry.group = group;
+ if (objectType == TYPE_LINK) {
+ entry.info = info;
+ }
+
+ mEntryList.add(entry);
+ }
+ }
+
+ /**
+ * Queries for an already existing Entry per name
+ * @param name the name of the entry
+ * @return the existing FileEntry or null if no entry with a matching
+ * name exists.
+ */
+ private FileEntry getExistingEntry(String name) {
+ for (int i = 0 ; i < mCurrentChildren.length; i++) {
+ FileEntry e = mCurrentChildren[i];
+
+ // since we're going to "erase" the one we use, we need to
+ // check that the item is not null.
+ if (e != null) {
+ // compare per name, case-sensitive.
+ if (name.equals(e.name)) {
+ // erase from the list
+ mCurrentChildren[i] = null;
+
+ // and return the object
+ return e;
+ }
+ }
+ }
+
+ // couldn't find any matching object, return null
+ return null;
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public void finishLinks() {
+ // TODO Handle links in the listing service
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide a method that deals with asynchronous
+ * result from <code>ls</code> command on the device.
+ *
+ * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver)
+ */
+ public interface IListingReceiver {
+ public void setChildren(FileEntry entry, FileEntry[] children);
+
+ public void refreshEntry(FileEntry entry);
+ }
+
+ /**
+ * Creates a File Listing Service for a specified {@link Device}.
+ * @param device The Device the service is connected to.
+ */
+ FileListingService(Device device) {
+ mDevice = device;
+ }
+
+ /**
+ * Returns the root element.
+ * @return the {@link FileEntry} object representing the root element or
+ * <code>null</code> if the device is invalid.
+ */
+ public FileEntry getRoot() {
+ if (mDevice != null) {
+ if (mRoot == null) {
+ mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY,
+ true /* isRoot */);
+ }
+
+ return mRoot;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the children of a {@link FileEntry}.
+ * <p/>
+ * This method supports a cache mechanism and synchronous and asynchronous modes.
+ * <p/>
+ * If <var>receiver</var> is <code>null</code>, the device side <code>ls</code>
+ * command is done synchronously, and the method will return upon completion of the command.<br>
+ * If <var>receiver</var> is non <code>null</code>, the command is launched is a separate
+ * thread and upon completion, the receiver will be notified of the result.
+ * <p/>
+ * The result for each <code>ls</code> command is cached in the parent
+ * <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the
+ * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms.
+ * After that a new <code>ls</code> command is always executed.
+ * <p/>
+ * If the cache is valid and <code>useCache == true</code>, the method will always simply
+ * return the value of the cache, whether a {@link IListingReceiver} has been provided or not.
+ *
+ * @param entry The parent entry.
+ * @param useCache A flag to use the cache or to force a new ls command.
+ * @param receiver A receiver for asynchronous calls.
+ * @return The list of children or <code>null</code> for asynchronous calls.
+ *
+ * @see FileEntry#getCachedChildren()
+ */
+ public FileEntry[] getChildren(final FileEntry entry, boolean useCache,
+ final IListingReceiver receiver) {
+ // first thing we do is check the cache, and if we already have a recent
+ // enough children list, we just return that.
+ if (useCache && entry.needFetch() == false) {
+ return entry.getCachedChildren();
+ }
+
+ // if there's no receiver, then this is a synchronous call, and we
+ // return the result of ls
+ if (receiver == null) {
+ doLs(entry);
+ return entry.getCachedChildren();
+ }
+
+ // this is a asynchronous call.
+ // we launch a thread that will do ls and give the listing
+ // to the receiver
+ Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ doLs(entry);
+
+ receiver.setChildren(entry, entry.getCachedChildren());
+
+ final FileEntry[] children = entry.getCachedChildren();
+ if (children.length > 0 && children[0].isApplicationPackage()) {
+ final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>();
+
+ for (FileEntry child : children) {
+ String path = child.getFullPath();
+ map.put(path, child);
+ }
+
+ // call pm.
+ String command = PM_FULL_LISTING;
+ try {
+ mDevice.executeShellCommand(command, new MultiLineReceiver() {
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.length() > 0) {
+ // get the filepath and package from the line
+ Matcher m = sPmPattern.matcher(line);
+ if (m.matches()) {
+ // get the children with that path
+ FileEntry entry = map.get(m.group(1));
+ if (entry != null) {
+ entry.info = m.group(2);
+ receiver.refreshEntry(entry);
+ }
+ }
+ }
+ }
+ }
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ // adb failed somehow, we do nothing.
+ }
+ }
+
+
+ // if another thread is pending, launch it
+ synchronized (mThreadList) {
+ // first remove ourselves from the list
+ mThreadList.remove(this);
+
+ // then launch the next one if applicable.
+ if (mThreadList.size() > 0) {
+ Thread t = mThreadList.get(0);
+ t.start();
+ }
+ }
+ }
+ };
+
+ // we don't want to run multiple ls on the device at the same time, so we
+ // store the thread in a list and launch it only if there's no other thread running.
+ // the thread will launch the next one once it's done.
+ synchronized (mThreadList) {
+ // add to the list
+ mThreadList.add(t);
+
+ // if it's the only one, launch it.
+ if (mThreadList.size() == 1) {
+ t.start();
+ }
+ }
+
+ // and we return null.
+ return null;
+ }
+
+ private void doLs(FileEntry entry) {
+ // create a list that will receive the list of the entries
+ ArrayList<FileEntry> entryList = new ArrayList<FileEntry>();
+
+ // create a list that will receive the link to compute post ls;
+ ArrayList<String> linkList = new ArrayList<String>();
+
+ try {
+ // create the command
+ String command = "ls -l " + entry.getFullPath(); //$NON-NLS-1$
+
+ // create the receiver object that will parse the result from ls
+ LsReceiver receiver = new LsReceiver(entry, entryList, linkList);
+
+ // call ls.
+ mDevice.executeShellCommand(command, receiver);
+
+ // finish the process of the receiver to handle links
+ receiver.finishLinks();
+ } catch (IOException e) {
+ }
+
+
+ // at this point we need to refresh the viewer
+ entry.fetchTime = System.currentTimeMillis();
+
+ // sort the children and set them as the new children
+ Collections.sort(entryList, FileEntry.sEntryComparator);
+ entry.setChildren(entryList);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java
new file mode 100644
index 0000000..9293379
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A receiver able to parse the result of the execution of
+ * {@link #GETPROP_COMMAND} on a device.
+ */
+final class GetPropReceiver extends MultiLineReceiver {
+ final static String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$
+
+ private final static Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$
+
+ /** indicates if we need to read the first */
+ private Device mDevice = null;
+
+ /**
+ * Creates the receiver with the device the receiver will modify.
+ * @param device The device to modify
+ */
+ public GetPropReceiver(Device device) {
+ mDevice = device;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ // We receive an array of lines. We're expecting
+ // to have the build info in the first line, and the build
+ // date in the 2nd line. There seems to be an empty line
+ // after all that.
+
+ for (String line : lines) {
+ if (line.length() == 0 || line.startsWith("#")) {
+ continue;
+ }
+
+ Matcher m = GETPROP_PATTERN.matcher(line);
+ if (m.matches()) {
+ String label = m.group(1);
+ String value = m.group(2);
+
+ if (label.length() > 0) {
+ mDevice.addProperty(label, value);
+ }
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void done() {
+ mDevice.update(Device.CHANGE_BUILD_INFO);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java
new file mode 100644
index 0000000..99bd4d0
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "app name" chunk (APNM).
+ */
+final class HandleAppName extends ChunkHandler {
+
+ public static final int CHUNK_APNM = ChunkHandler.type("APNM");
+
+ private static final HandleAppName mInst = new HandleAppName();
+
+
+ private HandleAppName() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_APNM, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {}
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data,
+ boolean isReply, int msgId) {
+
+ Log.d("ddm-appname", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_APNM) {
+ assert !isReply;
+ handleAPNM(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a reply to our APNM message.
+ */
+ private static void handleAPNM(Client client, ByteBuffer data) {
+ int appNameLen;
+ String appName;
+
+ appNameLen = data.getInt();
+ appName = getString(data, appNameLen);
+
+ Log.i("ddm-appname", "APNM: app='" + appName + "'");
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ cd.setClientDescription(appName);
+ }
+
+ client = checkDebuggerPortForAppName(client, appName);
+
+ if (client != null) {
+ client.update(Client.CHANGE_NAME);
+ }
+ }
+ }
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java
new file mode 100644
index 0000000..adeedbb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Submit an exit request.
+ */
+final class HandleExit extends ChunkHandler {
+
+ public static final int CHUNK_EXIT = type("EXIT");
+
+ private static final HandleExit mInst = new HandleExit();
+
+
+ private HandleExit() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {}
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {}
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+
+ /**
+ * Send an EXIT request to the client.
+ */
+ public static void sendEXIT(Client client, int status)
+ throws IOException
+ {
+ ByteBuffer rawBuf = allocBuffer(4);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.putInt(status);
+
+ finishChunkPacket(packet, CHUNK_EXIT, buf.position());
+ Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status);
+ client.sendAndConsume(packet, mInst);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java
new file mode 100644
index 0000000..5752b86
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Handle heap status updates.
+ */
+final class HandleHeap extends ChunkHandler {
+
+ public static final int CHUNK_HPIF = type("HPIF");
+ public static final int CHUNK_HPST = type("HPST");
+ public static final int CHUNK_HPEN = type("HPEN");
+ public static final int CHUNK_HPSG = type("HPSG");
+ public static final int CHUNK_HPGC = type("HPGC");
+ public static final int CHUNK_REAE = type("REAE");
+ public static final int CHUNK_REAQ = type("REAQ");
+ public static final int CHUNK_REAL = type("REAL");
+
+ // args to sendHPSG
+ public static final int WHEN_DISABLE = 0;
+ public static final int WHEN_GC = 1;
+ public static final int WHAT_MERGE = 0; // merge adjacent objects
+ public static final int WHAT_OBJ = 1; // keep objects distinct
+
+ // args to sendHPIF
+ public static final int HPIF_WHEN_NEVER = 0;
+ public static final int HPIF_WHEN_NOW = 1;
+ public static final int HPIF_WHEN_NEXT_GC = 2;
+ public static final int HPIF_WHEN_EVERY_GC = 3;
+
+ private static final HandleHeap mInst = new HandleHeap();
+
+ private HandleHeap() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_HPIF, mInst);
+ mt.registerChunkHandler(CHUNK_HPST, mInst);
+ mt.registerChunkHandler(CHUNK_HPEN, mInst);
+ mt.registerChunkHandler(CHUNK_HPSG, mInst);
+ mt.registerChunkHandler(CHUNK_REAQ, mInst);
+ mt.registerChunkHandler(CHUNK_REAL, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {
+ if (client.isHeapUpdateEnabled()) {
+ //sendHPSG(client, WHEN_GC, WHAT_MERGE);
+ sendHPIF(client, HPIF_WHEN_EVERY_GC);
+ }
+ }
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+ Log.d("ddm-heap", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_HPIF) {
+ handleHPIF(client, data);
+ client.update(Client.CHANGE_HEAP_DATA);
+ } else if (type == CHUNK_HPST) {
+ handleHPST(client, data);
+ } else if (type == CHUNK_HPEN) {
+ handleHPEN(client, data);
+ client.update(Client.CHANGE_HEAP_DATA);
+ } else if (type == CHUNK_HPSG) {
+ handleHPSG(client, data);
+ } else if (type == CHUNK_REAQ) {
+ handleREAQ(client, data);
+ client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS);
+ } else if (type == CHUNK_REAL) {
+ handleREAL(client, data);
+ client.update(Client.CHANGE_HEAP_ALLOCATIONS);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a heap info message.
+ */
+ private void handleHPIF(Client client, ByteBuffer data) {
+ Log.d("ddm-heap", "HPIF!");
+ try {
+ int numHeaps = data.getInt();
+
+ for (int i = 0; i < numHeaps; i++) {
+ int heapId = data.getInt();
+ @SuppressWarnings("unused")
+ long timeStamp = data.getLong();
+ @SuppressWarnings("unused")
+ byte reason = data.get();
+ long maxHeapSize = (long)data.getInt() & 0x00ffffffff;
+ long heapSize = (long)data.getInt() & 0x00ffffffff;
+ long bytesAllocated = (long)data.getInt() & 0x00ffffffff;
+ long objectsAllocated = (long)data.getInt() & 0x00ffffffff;
+
+ client.getClientData().setHeapInfo(heapId, maxHeapSize,
+ heapSize, bytesAllocated, objectsAllocated);
+ }
+ } catch (BufferUnderflowException ex) {
+ Log.w("ddm-heap", "malformed HPIF chunk from client");
+ }
+ }
+
+ /**
+ * Send an HPIF (HeaP InFo) request to the client.
+ */
+ public static void sendHPIF(Client client, int when) throws IOException {
+ ByteBuffer rawBuf = allocBuffer(1);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.put((byte)when);
+
+ finishChunkPacket(packet, CHUNK_HPIF, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when);
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /*
+ * Handle a heap segment series start message.
+ */
+ private void handleHPST(Client client, ByteBuffer data) {
+ /* Clear out any data that's sitting around to
+ * get ready for the chunks that are about to come.
+ */
+//xxx todo: only clear data that belongs to the heap mentioned in <data>.
+ client.getClientData().getVmHeapData().clearHeapData();
+ }
+
+ /*
+ * Handle a heap segment series end message.
+ */
+ private void handleHPEN(Client client, ByteBuffer data) {
+ /* Let the UI know that we've received all of the
+ * data for this heap.
+ */
+//xxx todo: only seal data that belongs to the heap mentioned in <data>.
+ client.getClientData().getVmHeapData().sealHeapData();
+ }
+
+ /*
+ * Handle a heap segment message.
+ */
+ private void handleHPSG(Client client, ByteBuffer data) {
+ byte dataCopy[] = new byte[data.limit()];
+ data.rewind();
+ data.get(dataCopy);
+ data = ByteBuffer.wrap(dataCopy);
+ client.getClientData().getVmHeapData().addHeapData(data);
+//xxx todo: add to the heap mentioned in <data>
+ }
+
+ /**
+ * Sends an HPSG (HeaP SeGment) request to the client.
+ */
+ public static void sendHPSG(Client client, int when, int what)
+ throws IOException {
+
+ ByteBuffer rawBuf = allocBuffer(2);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.put((byte)when);
+ buf.put((byte)what);
+
+ finishChunkPacket(packet, CHUNK_HPSG, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when="
+ + when + ", what=" + what);
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /**
+ * Sends an HPGC request to the client.
+ */
+ public static void sendHPGC(Client client)
+ throws IOException {
+ ByteBuffer rawBuf = allocBuffer(0);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ // no data
+
+ finishChunkPacket(packet, CHUNK_HPGC, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC));
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /**
+ * Sends a REAE (REcent Allocation Enable) request to the client.
+ */
+ public static void sendREAE(Client client, boolean enable)
+ throws IOException {
+ ByteBuffer rawBuf = allocBuffer(1);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.put((byte) (enable ? 1 : 0));
+
+ finishChunkPacket(packet, CHUNK_REAE, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable);
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /**
+ * Sends a REAQ (REcent Allocation Query) request to the client.
+ */
+ public static void sendREAQ(Client client)
+ throws IOException {
+ ByteBuffer rawBuf = allocBuffer(0);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ // no data
+
+ finishChunkPacket(packet, CHUNK_REAQ, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ));
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /**
+ * Sends a REAL (REcent ALlocation) request to the client.
+ */
+ public static void sendREAL(Client client)
+ throws IOException {
+ ByteBuffer rawBuf = allocBuffer(0);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ // no data
+
+ finishChunkPacket(packet, CHUNK_REAL, buf.position());
+ Log.d("ddm-heap", "Sending " + name(CHUNK_REAL));
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /*
+ * Handle the response from our REcent Allocation Query message.
+ */
+ private void handleREAQ(Client client, ByteBuffer data) {
+ boolean enabled;
+
+ enabled = (data.get() != 0);
+ Log.d("ddm-heap", "REAQ says: enabled=" + enabled);
+
+ client.getClientData().setAllocationStatus(enabled);
+ }
+
+ /**
+ * Converts a VM class descriptor string ("Landroid/os/Debug;") to
+ * a dot-notation class name ("android.os.Debug").
+ */
+ private String descriptorToDot(String str) {
+ // count the number of arrays.
+ int array = 0;
+ while (str.startsWith("[")) {
+ str = str.substring(1);
+ array++;
+ }
+
+ int len = str.length();
+
+ /* strip off leading 'L' and trailing ';' if appropriate */
+ if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') {
+ str = str.substring(1, len-1);
+ str = str.replace('/', '.');
+ } else {
+ // convert the basic types
+ if ("C".equals(str)) {
+ str = "char";
+ } else if ("B".equals(str)) {
+ str = "byte";
+ } else if ("Z".equals(str)) {
+ str = "boolean";
+ } else if ("S".equals(str)) {
+ str = "short";
+ } else if ("I".equals(str)) {
+ str = "int";
+ } else if ("J".equals(str)) {
+ str = "long";
+ } else if ("F".equals(str)) {
+ str = "float";
+ } else if ("D".equals(str)) {
+ str = "double";
+ }
+ }
+
+ // now add the array part
+ for (int a = 0 ; a < array; a++) {
+ str = str + "[]";
+ }
+
+ return str;
+ }
+
+ /**
+ * Reads a string table out of "data".
+ *
+ * This is just a serial collection of strings, each of which is a
+ * four-byte length followed by UTF-16 data.
+ */
+ private void readStringTable(ByteBuffer data, String[] strings) {
+ int count = strings.length;
+ int i;
+
+ for (i = 0; i < count; i++) {
+ int nameLen = data.getInt();
+ String descriptor = getString(data, nameLen);
+ strings[i] = descriptorToDot(descriptor);
+ }
+ }
+
+ /*
+ * Handle a REcent ALlocation response.
+ *
+ * Message header (all values big-endian):
+ * (1b) message header len (to allow future expansion); includes itself
+ * (1b) entry header len
+ * (1b) stack frame len
+ * (2b) number of entries
+ * (4b) offset to string table from start of message
+ * (2b) number of class name strings
+ * (2b) number of method name strings
+ * (2b) number of source file name strings
+ * For each entry:
+ * (4b) total allocation size
+ * (2b) threadId
+ * (2b) allocated object's class name index
+ * (1b) stack depth
+ * For each stack frame:
+ * (2b) method's class name
+ * (2b) method name
+ * (2b) method source file
+ * (2b) line number, clipped to 32767; -2 if native; -1 if no source
+ * (xb) class name strings
+ * (xb) method name strings
+ * (xb) source file strings
+ *
+ * As with other DDM traffic, strings are sent as a 4-byte length
+ * followed by UTF-16 data.
+ */
+ private void handleREAL(Client client, ByteBuffer data) {
+ Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL));
+ int messageHdrLen, entryHdrLen, stackFrameLen;
+ int numEntries, offsetToStrings;
+ int numClassNames, numMethodNames, numFileNames;
+
+ /*
+ * Read the header.
+ */
+ messageHdrLen = (data.get() & 0xff);
+ entryHdrLen = (data.get() & 0xff);
+ stackFrameLen = (data.get() & 0xff);
+ numEntries = (data.getShort() & 0xffff);
+ offsetToStrings = data.getInt();
+ numClassNames = (data.getShort() & 0xffff);
+ numMethodNames = (data.getShort() & 0xffff);
+ numFileNames = (data.getShort() & 0xffff);
+
+
+ /*
+ * Skip forward to the strings and read them.
+ */
+ data.position(offsetToStrings);
+
+ String[] classNames = new String[numClassNames];
+ String[] methodNames = new String[numMethodNames];
+ String[] fileNames = new String[numFileNames];
+
+ readStringTable(data, classNames);
+ readStringTable(data, methodNames);
+ //System.out.println("METHODS: "
+ // + java.util.Arrays.deepToString(methodNames));
+ readStringTable(data, fileNames);
+
+ /*
+ * Skip back to a point just past the header and start reading
+ * entries.
+ */
+ data.position(messageHdrLen);
+
+ ArrayList<AllocationInfo> list = new ArrayList<AllocationInfo>(numEntries);
+ for (int i = 0; i < numEntries; i++) {
+ int totalSize;
+ int threadId, classNameIndex, stackDepth;
+
+ totalSize = data.getInt();
+ threadId = (data.getShort() & 0xffff);
+ classNameIndex = (data.getShort() & 0xffff);
+ stackDepth = (data.get() & 0xff);
+ /* we've consumed 9 bytes; gobble up any extra */
+ for (int skip = 9; skip < entryHdrLen; skip++)
+ data.get();
+
+ StackTraceElement[] steArray = new StackTraceElement[stackDepth];
+
+ /*
+ * Pull out the stack trace.
+ */
+ for (int sti = 0; sti < stackDepth; sti++) {
+ int methodClassNameIndex, methodNameIndex;
+ int methodSourceFileIndex;
+ short lineNumber;
+ String methodClassName, methodName, methodSourceFile;
+
+ methodClassNameIndex = (data.getShort() & 0xffff);
+ methodNameIndex = (data.getShort() & 0xffff);
+ methodSourceFileIndex = (data.getShort() & 0xffff);
+ lineNumber = data.getShort();
+
+ methodClassName = classNames[methodClassNameIndex];
+ methodName = methodNames[methodNameIndex];
+ methodSourceFile = fileNames[methodSourceFileIndex];
+
+ steArray[sti] = new StackTraceElement(methodClassName,
+ methodName, methodSourceFile, lineNumber);
+
+ /* we've consumed 8 bytes; gobble up any extra */
+ for (int skip = 9; skip < stackFrameLen; skip++)
+ data.get();
+ }
+
+ list.add(new AllocationInfo(classNames[classNameIndex],
+ totalSize, (short) threadId, steArray));
+ }
+
+ // sort biggest allocations first.
+ Collections.sort(list);
+
+ client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries]));
+ }
+
+ /*
+ * For debugging: dump the contents of an AllocRecord array.
+ *
+ * The array starts with the oldest known allocation and ends with
+ * the most recent allocation.
+ */
+ @SuppressWarnings("unused")
+ private static void dumpRecords(AllocationInfo[] records) {
+ System.out.println("Found " + records.length + " records:");
+
+ for (AllocationInfo rec: records) {
+ System.out.println("tid=" + rec.getThreadId() + " "
+ + rec.getAllocatedClass() + " (" + rec.getSize() + " bytes)");
+
+ for (StackTraceElement ste: rec.getStackTrace()) {
+ if (ste.isNativeMethod()) {
+ System.out.println(" " + ste.getClassName()
+ + "." + ste.getMethodName()
+ + " (Native method)");
+ } else {
+ System.out.println(" " + ste.getClassName()
+ + "." + ste.getMethodName()
+ + " (" + ste.getFileName()
+ + ":" + ste.getLineNumber() + ")");
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java
new file mode 100644
index 0000000..5ba5aeb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "hello" chunk (HELO).
+ */
+final class HandleHello extends ChunkHandler {
+
+ public static final int CHUNK_HELO = ChunkHandler.type("HELO");
+
+ private static final HandleHello mInst = new HandleHello();
+
+
+ private HandleHello() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_HELO, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {
+ Log.d("ddm-hello", "Now ready: " + client);
+ }
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {
+ Log.d("ddm-hello", "Now disconnected: " + client);
+ }
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+ Log.d("ddm-hello", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_HELO) {
+ assert isReply;
+ handleHELO(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a reply to our HELO message.
+ */
+ private static void handleHELO(Client client, ByteBuffer data) {
+ int version, pid, vmIdentLen, appNameLen;
+ String vmIdent, appName;
+
+ version = data.getInt();
+ pid = data.getInt();
+ vmIdentLen = data.getInt();
+ appNameLen = data.getInt();
+
+ vmIdent = getString(data, vmIdentLen);
+ appName = getString(data, appNameLen);
+
+ Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid
+ + ", vm='" + vmIdent + "', app='" + appName + "'");
+
+ ClientData cd = client.getClientData();
+
+ synchronized (cd) {
+ if (cd.getPid() == pid) {
+ cd.setVmIdentifier(vmIdent);
+ cd.setClientDescription(appName);
+ cd.isDdmAware(true);
+ } else {
+ Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid ("
+ + cd.getPid() + ")");
+ }
+ }
+
+ client = checkDebuggerPortForAppName(client, appName);
+
+ if (client != null) {
+ client.update(Client.CHANGE_NAME);
+ }
+ }
+
+
+ /**
+ * Send a HELO request to the client.
+ */
+ public static void sendHELO(Client client, int serverProtocolVersion)
+ throws IOException
+ {
+ ByteBuffer rawBuf = allocBuffer(4);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.putInt(serverProtocolVersion);
+
+ finishChunkPacket(packet, CHUNK_HELO, buf.position());
+ Log.d("ddm-hello", "Sending " + name(CHUNK_HELO)
+ + " ID=0x" + Integer.toHexString(packet.getId()));
+ client.sendAndConsume(packet, mInst);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java
new file mode 100644
index 0000000..ca26590
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleNativeHeap extends ChunkHandler {
+
+ public static final int CHUNK_NHGT = type("NHGT"); // $NON-NLS-1$
+ public static final int CHUNK_NHSG = type("NHSG"); // $NON-NLS-1$
+ public static final int CHUNK_NHST = type("NHST"); // $NON-NLS-1$
+ public static final int CHUNK_NHEN = type("NHEN"); // $NON-NLS-1$
+
+ private static final HandleNativeHeap mInst = new HandleNativeHeap();
+
+ private HandleNativeHeap() {
+ }
+
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_NHGT, mInst);
+ mt.registerChunkHandler(CHUNK_NHSG, mInst);
+ mt.registerChunkHandler(CHUNK_NHST, mInst);
+ mt.registerChunkHandler(CHUNK_NHEN, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {}
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+ Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_NHGT) {
+ handleNHGT(client, data);
+ } else if (type == CHUNK_NHST) {
+ // start chunk before any NHSG chunk(s)
+ client.getClientData().getNativeHeapData().clearHeapData();
+ } else if (type == CHUNK_NHEN) {
+ // end chunk after NHSG chunk(s)
+ client.getClientData().getNativeHeapData().sealHeapData();
+ } else if (type == CHUNK_NHSG) {
+ handleNHSG(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+
+ client.update(Client.CHANGE_NATIVE_HEAP_DATA);
+ }
+
+ /**
+ * Send an NHGT (Native Thread GeT) request to the client.
+ */
+ public static void sendNHGT(Client client) throws IOException {
+
+ ByteBuffer rawBuf = allocBuffer(0);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ // no data in request message
+
+ finishChunkPacket(packet, CHUNK_NHGT, buf.position());
+ Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT));
+ client.sendAndConsume(packet, mInst);
+
+ rawBuf = allocBuffer(2);
+ packet = new JdwpPacket(rawBuf);
+ buf = getChunkDataBuf(rawBuf);
+
+ buf.put((byte)HandleHeap.WHEN_GC);
+ buf.put((byte)HandleHeap.WHAT_OBJ);
+
+ finishChunkPacket(packet, CHUNK_NHSG, buf.position());
+ Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG));
+ client.sendAndConsume(packet, mInst);
+ }
+
+ /*
+ * Handle our native heap data.
+ */
+ private void handleNHGT(Client client, ByteBuffer data) {
+ ClientData cd = client.getClientData();
+
+ Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes");
+
+ // TODO - process incoming data and save in "cd"
+ byte[] copy = new byte[data.limit()];
+ data.get(copy);
+
+ // clear the previous run
+ cd.clearNativeAllocationInfo();
+
+ ByteBuffer buffer = ByteBuffer.wrap(copy);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+// read the header
+// typedef struct Header {
+// uint32_t mapSize;
+// uint32_t allocSize;
+// uint32_t allocInfoSize;
+// uint32_t totalMemory;
+// uint32_t backtraceSize;
+// };
+
+ int mapSize = buffer.getInt();
+ int allocSize = buffer.getInt();
+ int allocInfoSize = buffer.getInt();
+ int totalMemory = buffer.getInt();
+ int backtraceSize = buffer.getInt();
+
+ Log.d("ddms", "mapSize: " + mapSize);
+ Log.d("ddms", "allocSize: " + allocSize);
+ Log.d("ddms", "allocInfoSize: " + allocInfoSize);
+ Log.d("ddms", "totalMemory: " + totalMemory);
+
+ cd.setTotalNativeMemory(totalMemory);
+
+ // this means that updates aren't turned on.
+ if (allocInfoSize == 0)
+ return;
+
+ if (mapSize > 0) {
+ byte[] maps = new byte[mapSize];
+ buffer.get(maps, 0, mapSize);
+ parseMaps(cd, maps);
+ }
+
+ int iterations = allocSize / allocInfoSize;
+
+ for (int i = 0 ; i < iterations ; i++) {
+ NativeAllocationInfo info = new NativeAllocationInfo(
+ buffer.getInt() /* size */,
+ buffer.getInt() /* allocations */);
+
+ for (int j = 0 ; j < backtraceSize ; j++) {
+ long addr = ((long)buffer.getInt()) & 0x00000000ffffffffL;
+
+ info.addStackCallAddress(addr);;
+ }
+
+ cd.addNativeAllocation(info);
+ }
+ }
+
+ private void handleNHSG(Client client, ByteBuffer data) {
+ byte dataCopy[] = new byte[data.limit()];
+ data.rewind();
+ data.get(dataCopy);
+ data = ByteBuffer.wrap(dataCopy);
+ client.getClientData().getNativeHeapData().addHeapData(data);
+
+ if (true) {
+ return;
+ }
+
+ // WORK IN PROGRESS
+
+// Log.e("ddm-nativeheap", "NHSG: ----------------------------------");
+// Log.e("ddm-nativeheap", "NHSG: " + data.limit() + " bytes");
+
+ byte[] copy = new byte[data.limit()];
+ data.get(copy);
+
+ ByteBuffer buffer = ByteBuffer.wrap(copy);
+ buffer.order(ByteOrder.BIG_ENDIAN);
+
+ int id = buffer.getInt();
+ int unitsize = (int) buffer.get();
+ long startAddress = (long) buffer.getInt() & 0x00000000ffffffffL;
+ int offset = buffer.getInt();
+ int allocationUnitCount = buffer.getInt();
+
+// Log.e("ddm-nativeheap", "id: " + id);
+// Log.e("ddm-nativeheap", "unitsize: " + unitsize);
+// Log.e("ddm-nativeheap", "startAddress: 0x" + Long.toHexString(startAddress));
+// Log.e("ddm-nativeheap", "offset: " + offset);
+// Log.e("ddm-nativeheap", "allocationUnitCount: " + allocationUnitCount);
+// Log.e("ddm-nativeheap", "end: 0x" +
+// Long.toHexString(startAddress + unitsize * allocationUnitCount));
+
+ // read the usage
+ while (buffer.position() < buffer.limit()) {
+ int eState = (int)buffer.get() & 0x000000ff;
+ int eLen = ((int)buffer.get() & 0x000000ff) + 1;
+ //Log.e("ddm-nativeheap", "solidity: " + (eState & 0x7) + " - kind: "
+ // + ((eState >> 3) & 0x7) + " - len: " + eLen);
+ }
+
+
+// count += unitsize * allocationUnitCount;
+// Log.e("ddm-nativeheap", "count = " + count);
+
+ }
+
+ private void parseMaps(ClientData cd, byte[] maps) {
+ InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps));
+ BufferedReader reader = new BufferedReader(input);
+
+ String line;
+
+ try {
+
+ // most libraries are defined on several lines, so we need to make sure we parse
+ // all the library lines and only add the library at the end
+ long startAddr = 0;
+ long endAddr = 0;
+ String library = null;
+
+ while ((line = reader.readLine()) != null) {
+ Log.d("ddms", "line: " + line);
+ if (line.length() < 16) {
+ continue;
+ }
+
+ try {
+ long tmpStart = Long.parseLong(line.substring(0, 8), 16);
+ long tmpEnd = Long.parseLong(line.substring(9, 17), 16);
+
+ /*
+ * only check for library addresses as defined in
+ * //device/config/prelink-linux-arm.map
+ */
+ if (tmpStart >= 0x0000000080000000L && tmpStart <= 0x00000000BFFFFFFFL) {
+
+ int index = line.indexOf('/');
+
+ if (index == -1)
+ continue;
+
+ String tmpLib = line.substring(index);
+
+ if (library == null ||
+ (library != null && tmpLib.equals(library) == false)) {
+
+ if (library != null) {
+ cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
+ Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
+ " - " + Long.toHexString(endAddr) + ")");
+ }
+
+ // now init the new library
+ library = tmpLib;
+ startAddr = tmpStart;
+ endAddr = tmpEnd;
+ } else {
+ // add the new end
+ endAddr = tmpEnd;
+ }
+ }
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (library != null) {
+ cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
+ Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
+ " - " + Long.toHexString(endAddr) + ")");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java
new file mode 100644
index 0000000..b9f3a74
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleTest extends ChunkHandler {
+
+ public static final int CHUNK_TEST = type("TEST");
+
+ private static final HandleTest mInst = new HandleTest();
+
+
+ private HandleTest() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_TEST, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {}
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+ Log.d("ddm-test", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_TEST) {
+ handleTEST(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a thread creation message.
+ */
+ private void handleTEST(Client client, ByteBuffer data)
+ {
+ /*
+ * Can't call data.array() on a read-only ByteBuffer, so we make
+ * a copy.
+ */
+ byte[] copy = new byte[data.limit()];
+ data.get(copy);
+
+ Log.d("ddm-test", "Received:");
+ Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java
new file mode 100644
index 0000000..572eed2
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleThread extends ChunkHandler {
+
+ public static final int CHUNK_THEN = type("THEN");
+ public static final int CHUNK_THCR = type("THCR");
+ public static final int CHUNK_THDE = type("THDE");
+ public static final int CHUNK_THST = type("THST");
+ public static final int CHUNK_THNM = type("THNM");
+ public static final int CHUNK_STKL = type("STKL");
+
+ private static final HandleThread mInst = new HandleThread();
+
+ // only read/written by requestThreadUpdates()
+ private static volatile boolean mThreadStatusReqRunning = false;
+ private static volatile boolean mThreadStackTraceReqRunning = false;
+
+ private HandleThread() {}
+
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_THCR, mInst);
+ mt.registerChunkHandler(CHUNK_THDE, mInst);
+ mt.registerChunkHandler(CHUNK_THST, mInst);
+ mt.registerChunkHandler(CHUNK_THNM, mInst);
+ mt.registerChunkHandler(CHUNK_STKL, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {
+ Log.d("ddm-thread", "Now ready: " + client);
+ if (client.isThreadUpdateEnabled())
+ sendTHEN(client, true);
+ }
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+ Log.d("ddm-thread", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_THCR) {
+ handleTHCR(client, data);
+ } else if (type == CHUNK_THDE) {
+ handleTHDE(client, data);
+ } else if (type == CHUNK_THST) {
+ handleTHST(client, data);
+ } else if (type == CHUNK_THNM) {
+ handleTHNM(client, data);
+ } else if (type == CHUNK_STKL) {
+ handleSTKL(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a thread creation message.
+ *
+ * We should be tolerant of receiving a duplicate create message. (It
+ * shouldn't happen with the current implementation.)
+ */
+ private void handleTHCR(Client client, ByteBuffer data) {
+ int threadId, nameLen;
+ String name;
+
+ threadId = data.getInt();
+ nameLen = data.getInt();
+ name = getString(data, nameLen);
+
+ Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'");
+
+ client.getClientData().addThread(threadId, name);
+ client.update(Client.CHANGE_THREAD_DATA);
+ }
+
+ /*
+ * Handle a thread death message.
+ */
+ private void handleTHDE(Client client, ByteBuffer data) {
+ int threadId;
+
+ threadId = data.getInt();
+ Log.v("ddm-thread", "THDE: " + threadId);
+
+ client.getClientData().removeThread(threadId);
+ client.update(Client.CHANGE_THREAD_DATA);
+ }
+
+ /*
+ * Handle a thread status update message.
+ *
+ * Response has:
+ * (1b) header len
+ * (1b) bytes per entry
+ * (2b) thread count
+ * Then, for each thread:
+ * (4b) threadId (matches value from THCR)
+ * (1b) thread status
+ * (4b) tid
+ * (4b) utime
+ * (4b) stime
+ */
+ private void handleTHST(Client client, ByteBuffer data) {
+ int headerLen, bytesPerEntry, extraPerEntry;
+ int threadCount;
+
+ headerLen = (data.get() & 0xff);
+ bytesPerEntry = (data.get() & 0xff);
+ threadCount = data.getShort();
+
+ headerLen -= 4; // we've read 4 bytes
+ while (headerLen-- > 0)
+ data.get();
+
+ extraPerEntry = bytesPerEntry - 18; // we want 18 bytes
+
+ Log.v("ddm-thread", "THST: threadCount=" + threadCount);
+
+ /*
+ * For each thread, extract the data, find the appropriate
+ * client, and add it to the ClientData.
+ */
+ for (int i = 0; i < threadCount; i++) {
+ int threadId, status, tid, utime, stime;
+ boolean isDaemon = false;
+
+ threadId = data.getInt();
+ status = data.get();
+ tid = data.getInt();
+ utime = data.getInt();
+ stime = data.getInt();
+ if (bytesPerEntry >= 18)
+ isDaemon = (data.get() != 0);
+
+ Log.v("ddm-thread", " id=" + threadId
+ + ", status=" + status + ", tid=" + tid
+ + ", utime=" + utime + ", stime=" + stime);
+
+ ClientData cd = client.getClientData();
+ ThreadInfo threadInfo = cd.getThread(threadId);
+ if (threadInfo != null)
+ threadInfo.updateThread(status, tid, utime, stime, isDaemon);
+ else
+ Log.i("ddms", "Thread with id=" + threadId + " not found");
+
+ // slurp up any extra
+ for (int slurp = extraPerEntry; slurp > 0; slurp--)
+ data.get();
+ }
+
+ client.update(Client.CHANGE_THREAD_DATA);
+ }
+
+ /*
+ * Handle a THNM (THread NaMe) message. We get one of these after
+ * somebody calls Thread.setName() on a running thread.
+ */
+ private void handleTHNM(Client client, ByteBuffer data) {
+ int threadId, nameLen;
+ String name;
+
+ threadId = data.getInt();
+ nameLen = data.getInt();
+ name = getString(data, nameLen);
+
+ Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'");
+
+ ThreadInfo threadInfo = client.getClientData().getThread(threadId);
+ if (threadInfo != null) {
+ threadInfo.setThreadName(name);
+ client.update(Client.CHANGE_THREAD_DATA);
+ } else {
+ Log.i("ddms", "Thread with id=" + threadId + " not found");
+ }
+ }
+
+
+ /**
+ * Parse an incoming STKL.
+ */
+ private void handleSTKL(Client client, ByteBuffer data) {
+ StackTraceElement[] trace;
+ int i, threadId, stackDepth;
+ @SuppressWarnings("unused")
+ int future;
+
+ future = data.getInt();
+ threadId = data.getInt();
+
+ Log.v("ddms", "STKL: " + threadId);
+
+ /* un-serialize the StackTraceElement[] */
+ stackDepth = data.getInt();
+ trace = new StackTraceElement[stackDepth];
+ for (i = 0; i < stackDepth; i++) {
+ String className, methodName, fileName;
+ int len, lineNumber;
+
+ len = data.getInt();
+ className = getString(data, len);
+ len = data.getInt();
+ methodName = getString(data, len);
+ len = data.getInt();
+ if (len == 0) {
+ fileName = null;
+ } else {
+ fileName = getString(data, len);
+ }
+ lineNumber = data.getInt();
+
+ trace[i] = new StackTraceElement(className, methodName, fileName,
+ lineNumber);
+ }
+
+ ThreadInfo threadInfo = client.getClientData().getThread(threadId);
+ if (threadInfo != null) {
+ threadInfo.setStackCall(trace);
+ client.update(Client.CHANGE_THREAD_STACKTRACE);
+ } else {
+ Log.d("STKL", String.format(
+ "Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$
+ threadId));
+ }
+ }
+
+
+ /**
+ * Send a THEN (THread notification ENable) request to the client.
+ */
+ public static void sendTHEN(Client client, boolean enable)
+ throws IOException {
+
+ ByteBuffer rawBuf = allocBuffer(1);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ if (enable)
+ buf.put((byte)1);
+ else
+ buf.put((byte)0);
+
+ finishChunkPacket(packet, CHUNK_THEN, buf.position());
+ Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable);
+ client.sendAndConsume(packet, mInst);
+ }
+
+
+ /**
+ * Send a STKL (STacK List) request to the client. The VM will suspend
+ * the target thread, obtain its stack, and return it. If the thread
+ * is no longer running, a failure result will be returned.
+ */
+ public static void sendSTKL(Client client, int threadId)
+ throws IOException {
+
+ if (false) {
+ Log.i("ddm-thread", "would send STKL " + threadId);
+ return;
+ }
+
+ ByteBuffer rawBuf = allocBuffer(4);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ buf.putInt(threadId);
+
+ finishChunkPacket(packet, CHUNK_STKL, buf.position());
+ Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId);
+ client.sendAndConsume(packet, mInst);
+ }
+
+
+ /**
+ * This is called periodically from the UI thread. To avoid locking
+ * the UI while we request the updates, we create a new thread.
+ *
+ */
+ static void requestThreadUpdate(final Client client) {
+ if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
+ if (mThreadStatusReqRunning) {
+ Log.w("ddms", "Waiting for previous thread update req to finish");
+ return;
+ }
+
+ new Thread("Thread Status Req") {
+ @Override
+ public void run() {
+ mThreadStatusReqRunning = true;
+ try {
+ sendTHST(client);
+ } catch (IOException ioe) {
+ Log.i("ddms", "Unable to request thread updates from "
+ + client + ": " + ioe.getMessage());
+ } finally {
+ mThreadStatusReqRunning = false;
+ }
+ }
+ }.start();
+ }
+ }
+
+ static void requestThreadStackCallRefresh(final Client client, final int threadId) {
+ if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
+ if (mThreadStackTraceReqRunning ) {
+ Log.w("ddms", "Waiting for previous thread stack call req to finish");
+ return;
+ }
+
+ new Thread("Thread Status Req") {
+ @Override
+ public void run() {
+ mThreadStackTraceReqRunning = true;
+ try {
+ sendSTKL(client, threadId);
+ } catch (IOException ioe) {
+ Log.i("ddms", "Unable to request thread stack call updates from "
+ + client + ": " + ioe.getMessage());
+ } finally {
+ mThreadStackTraceReqRunning = false;
+ }
+ }
+ }.start();
+ }
+
+ }
+
+ /*
+ * Send a THST request to the specified client.
+ */
+ private static void sendTHST(Client client) throws IOException {
+ ByteBuffer rawBuf = allocBuffer(0);
+ JdwpPacket packet = new JdwpPacket(rawBuf);
+ ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+ // nothing much to say
+
+ finishChunkPacket(packet, CHUNK_THST, buf.position());
+ Log.d("ddm-thread", "Sending " + name(CHUNK_THST));
+ client.sendAndConsume(packet, mInst);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java
new file mode 100644
index 0000000..d27e636
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "wait" chunk (WAIT). These are sent up when the client is
+ * waiting for something, e.g. for a debugger to attach.
+ */
+final class HandleWait extends ChunkHandler {
+
+ public static final int CHUNK_WAIT = ChunkHandler.type("WAIT");
+
+ private static final HandleWait mInst = new HandleWait();
+
+
+ private HandleWait() {}
+
+ /**
+ * Register for the packets we expect to get from the client.
+ */
+ public static void register(MonitorThread mt) {
+ mt.registerChunkHandler(CHUNK_WAIT, mInst);
+ }
+
+ /**
+ * Client is ready.
+ */
+ @Override
+ public void clientReady(Client client) throws IOException {}
+
+ /**
+ * Client went away.
+ */
+ @Override
+ public void clientDisconnected(Client client) {}
+
+ /**
+ * Chunk handler entry point.
+ */
+ @Override
+ public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+ Log.d("ddm-wait", "handling " + ChunkHandler.name(type));
+
+ if (type == CHUNK_WAIT) {
+ assert !isReply;
+ handleWAIT(client, data);
+ } else {
+ handleUnknownChunk(client, type, data, isReply, msgId);
+ }
+ }
+
+ /*
+ * Handle a reply to our WAIT message.
+ */
+ private static void handleWAIT(Client client, ByteBuffer data) {
+ byte reason;
+
+ reason = data.get();
+
+ Log.i("ddm-wait", "WAIT: reason=" + reason);
+
+
+ ClientData cd = client.getClientData();
+ synchronized (cd) {
+ cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_WAITING);
+ }
+
+ client.update(Client.CHANGE_DEBUGGER_INTEREST);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java
new file mode 100644
index 0000000..6a62e60
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.ParseException;
+
+/**
+ * Describes the types and locations of objects in a segment of a heap.
+ */
+public final class HeapSegment implements Comparable<HeapSegment> {
+
+ /**
+ * Describes an object/region encoded in the HPSG data.
+ */
+ public static class HeapSegmentElement implements Comparable<HeapSegmentElement> {
+
+ /*
+ * Solidity values, which must match the values in
+ * the HPSG data.
+ */
+
+ /** The element describes a free block. */
+ public static int SOLIDITY_FREE = 0;
+
+ /** The element is strongly-reachable. */
+ public static int SOLIDITY_HARD = 1;
+
+ /** The element is softly-reachable. */
+ public static int SOLIDITY_SOFT = 2;
+
+ /** The element is weakly-reachable. */
+ public static int SOLIDITY_WEAK = 3;
+
+ /** The element is phantom-reachable. */
+ public static int SOLIDITY_PHANTOM = 4;
+
+ /** The element is pending finalization. */
+ public static int SOLIDITY_FINALIZABLE = 5;
+
+ /** The element is not reachable, and is about to be swept/freed. */
+ public static int SOLIDITY_SWEEP = 6;
+
+ /** The reachability of the object is unknown. */
+ public static int SOLIDITY_INVALID = -1;
+
+
+ /*
+ * Kind values, which must match the values in
+ * the HPSG data.
+ */
+
+ /** The element describes a data object. */
+ public static int KIND_OBJECT = 0;
+
+ /** The element describes a class object. */
+ public static int KIND_CLASS_OBJECT = 1;
+
+ /** The element describes an array of 1-byte elements. */
+ public static int KIND_ARRAY_1 = 2;
+
+ /** The element describes an array of 2-byte elements. */
+ public static int KIND_ARRAY_2 = 3;
+
+ /** The element describes an array of 4-byte elements. */
+ public static int KIND_ARRAY_4 = 4;
+
+ /** The element describes an array of 8-byte elements. */
+ public static int KIND_ARRAY_8 = 5;
+
+ /** The element describes an unknown type of object. */
+ public static int KIND_UNKNOWN = 6;
+
+ /** The element describes a native object. */
+ public static int KIND_NATIVE = 7;
+
+ /** The object kind is unknown or unspecified. */
+ public static int KIND_INVALID = -1;
+
+
+ /**
+ * A bit in the HPSG data that indicates that an element should
+ * be combined with the element that follows, typically because
+ * an element is too large to be described by a single element.
+ */
+ private static int PARTIAL_MASK = 1 << 7;
+
+
+ /**
+ * Describes the reachability/solidity of the element. Must
+ * be set to one of the SOLIDITY_* values.
+ */
+ private int mSolidity;
+
+ /**
+ * Describes the type/kind of the element. Must be set to one
+ * of the KIND_* values.
+ */
+ private int mKind;
+
+ /**
+ * Describes the length of the element, in bytes.
+ */
+ private int mLength;
+
+
+ /**
+ * Creates an uninitialized element.
+ */
+ public HeapSegmentElement() {
+ setSolidity(SOLIDITY_INVALID);
+ setKind(KIND_INVALID);
+ setLength(-1);
+ }
+
+ /**
+ * Create an element describing the entry at the current
+ * position of hpsgData.
+ *
+ * @param hs The heap segment to pull the entry from.
+ * @throws BufferUnderflowException if there is not a whole entry
+ * following the current position
+ * of hpsgData.
+ * @throws ParseException if the provided data is malformed.
+ */
+ public HeapSegmentElement(HeapSegment hs)
+ throws BufferUnderflowException, ParseException {
+ set(hs);
+ }
+
+ /**
+ * Replace the element with the entry at the current position of
+ * hpsgData.
+ *
+ * @param hs The heap segment to pull the entry from.
+ * @return this object.
+ * @throws BufferUnderflowException if there is not a whole entry
+ * following the current position of
+ * hpsgData.
+ * @throws ParseException if the provided data is malformed.
+ */
+ public HeapSegmentElement set(HeapSegment hs)
+ throws BufferUnderflowException, ParseException {
+
+ /* TODO: Maybe keep track of the virtual address of each element
+ * so that they can be examined independently.
+ */
+ ByteBuffer data = hs.mUsageData;
+ int eState = (int)data.get() & 0x000000ff;
+ int eLen = ((int)data.get() & 0x000000ff) + 1;
+
+ while ((eState & PARTIAL_MASK) != 0) {
+
+ /* If the partial bit was set, the next byte should describe
+ * the same object as the current one.
+ */
+ int nextState = (int)data.get() & 0x000000ff;
+ if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
+ throw new ParseException("State mismatch", data.position());
+ }
+ eState = nextState;
+ eLen += ((int)data.get() & 0x000000ff) + 1;
+ }
+
+ setSolidity(eState & 0x7);
+ setKind((eState >> 3) & 0x7);
+ setLength(eLen * hs.mAllocationUnitSize);
+
+ return this;
+ }
+
+ public int getSolidity() {
+ return mSolidity;
+ }
+
+ public void setSolidity(int solidity) {
+ this.mSolidity = solidity;
+ }
+
+ public int getKind() {
+ return mKind;
+ }
+
+ public void setKind(int kind) {
+ this.mKind = kind;
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public void setLength(int length) {
+ this.mLength = length;
+ }
+
+ public int compareTo(HeapSegmentElement other) {
+ if (mLength != other.mLength) {
+ return mLength < other.mLength ? -1 : 1;
+ }
+ return 0;
+ }
+ }
+
+ //* The ID of the heap that this segment belongs to.
+ protected int mHeapId;
+
+ //* The size of an allocation unit, in bytes. (e.g., 8 bytes)
+ protected int mAllocationUnitSize;
+
+ //* The virtual address of the start of this segment.
+ protected long mStartAddress;
+
+ //* The offset of this pices from mStartAddress, in bytes.
+ protected int mOffset;
+
+ //* The number of allocation units described in this segment.
+ protected int mAllocationUnitCount;
+
+ //* The raw data that describes the contents of this segment.
+ protected ByteBuffer mUsageData;
+
+ //* mStartAddress is set to this value when the segment becomes invalid.
+ private final static long INVALID_START_ADDRESS = -1;
+
+ /**
+ * Create a new HeapSegment based on the raw contents
+ * of an HPSG chunk.
+ *
+ * @param hpsgData The raw data from an HPSG chunk.
+ * @throws BufferUnderflowException if hpsgData is too small
+ * to hold the HPSG chunk header data.
+ */
+ public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException {
+ /* Read the HPSG chunk header.
+ * These get*() calls may throw a BufferUnderflowException
+ * if the underlying data isn't big enough.
+ */
+ hpsgData.order(ByteOrder.BIG_ENDIAN);
+ mHeapId = hpsgData.getInt();
+ mAllocationUnitSize = (int) hpsgData.get();
+ mStartAddress = (long) hpsgData.getInt() & 0x00000000ffffffffL;
+ mOffset = hpsgData.getInt();
+ mAllocationUnitCount = hpsgData.getInt();
+
+ // Hold onto the remainder of the data.
+ mUsageData = hpsgData.slice();
+ mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter
+
+ // Validate the data.
+//xxx do it
+//xxx make sure the number of elements matches mAllocationUnitCount.
+//xxx make sure the last element doesn't have P set
+ }
+
+ /**
+ * See if this segment still contains data, and has not been
+ * appended to another segment.
+ *
+ * @return true if this segment has not been appended to
+ * another segment.
+ */
+ public boolean isValid() {
+ return mStartAddress != INVALID_START_ADDRESS;
+ }
+
+ /**
+ * See if <code>other</code> comes immediately after this segment.
+ *
+ * @param other The HeapSegment to check.
+ * @return true if <code>other</code> comes immediately after this
+ * segment.
+ */
+ public boolean canAppend(HeapSegment other) {
+ return isValid() && other.isValid() && mHeapId == other.mHeapId &&
+ mAllocationUnitSize == other.mAllocationUnitSize &&
+ getEndAddress() == other.getStartAddress();
+ }
+
+ /**
+ * Append the contents of <code>other</code> to this segment
+ * if it describes the segment immediately after this one.
+ *
+ * @param other The segment to append to this segment, if possible.
+ * If appended, <code>other</code> will be invalid
+ * when this method returns.
+ * @return true if <code>other</code> was successfully appended to
+ * this segment.
+ */
+ public boolean append(HeapSegment other) {
+ if (canAppend(other)) {
+ /* Preserve the position. The mark is not preserved,
+ * but we don't use it anyway.
+ */
+ int pos = mUsageData.position();
+
+ // Guarantee that we have enough room for the new data.
+ if (mUsageData.capacity() - mUsageData.limit() <
+ other.mUsageData.limit()) {
+ /* Grow more than necessary in case another append()
+ * is about to happen.
+ */
+ int newSize = mUsageData.limit() + other.mUsageData.limit();
+ ByteBuffer newData = ByteBuffer.allocate(newSize * 2);
+
+ mUsageData.rewind();
+ newData.put(mUsageData);
+ mUsageData = newData;
+ }
+
+ // Copy the data from the other segment and restore the position.
+ other.mUsageData.rewind();
+ mUsageData.put(other.mUsageData);
+ mUsageData.position(pos);
+
+ // Fix this segment's header to cover the new data.
+ mAllocationUnitCount += other.mAllocationUnitCount;
+
+ // Mark the other segment as invalid.
+ other.mStartAddress = INVALID_START_ADDRESS;
+ other.mUsageData = null;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public long getStartAddress() {
+ return mStartAddress + mOffset;
+ }
+
+ public int getLength() {
+ return mAllocationUnitSize * mAllocationUnitCount;
+ }
+
+ public long getEndAddress() {
+ return getStartAddress() + getLength();
+ }
+
+ public void rewindElements() {
+ if (mUsageData != null) {
+ mUsageData.rewind();
+ }
+ }
+
+ public HeapSegmentElement getNextElement(HeapSegmentElement reuse) {
+ try {
+ if (reuse != null) {
+ return reuse.set(this);
+ } else {
+ return new HeapSegmentElement(this);
+ }
+ } catch (BufferUnderflowException ex) {
+ /* Normal "end of buffer" situation.
+ */
+ } catch (ParseException ex) {
+ /* Malformed data.
+ */
+//TODO: we should catch this in the constructor
+ }
+ return null;
+ }
+
+ /*
+ * Method overrides for Comparable
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof HeapSegment) {
+ return compareTo((HeapSegment) o) == 0;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHeapId * 31 +
+ mAllocationUnitSize * 31 +
+ (int) mStartAddress * 31 +
+ mOffset * 31 +
+ mAllocationUnitCount * 31 +
+ mUsageData.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+
+ str.append("HeapSegment { heap ").append(mHeapId)
+ .append(", start 0x")
+ .append(Integer.toHexString((int) getStartAddress()))
+ .append(", length ").append(getLength())
+ .append(" }");
+
+ return str.toString();
+ }
+
+ public int compareTo(HeapSegment other) {
+ if (mHeapId != other.mHeapId) {
+ return mHeapId < other.mHeapId ? -1 : 1;
+ }
+ if (getStartAddress() != other.getStartAddress()) {
+ return getStartAddress() < other.getStartAddress() ? -1 : 1;
+ }
+
+ /* If two segments have the same start address, the rest of
+ * the fields should be equal. Go through the motions, though.
+ * Note that we re-check the components of getStartAddress()
+ * (mStartAddress and mOffset) to make sure that all fields in
+ * an equal segment are equal.
+ */
+
+ if (mAllocationUnitSize != other.mAllocationUnitSize) {
+ return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1;
+ }
+ if (mStartAddress != other.mStartAddress) {
+ return mStartAddress < other.mStartAddress ? -1 : 1;
+ }
+ if (mOffset != other.mOffset) {
+ return mOffset < other.mOffset ? -1 : 1;
+ }
+ if (mAllocationUnitCount != other.mAllocationUnitCount) {
+ return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1;
+ }
+ if (mUsageData != other.mUsageData) {
+ return mUsageData.compareTo(other.mUsageData);
+ }
+ return 0;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java
new file mode 100755
index 0000000..5dbce92
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.Device.DeviceState;
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.IOException;
+import java.util.Map;
+
+
+/**
+ * A Device. It can be a physical device or an emulator.
+ */
+public interface IDevice {
+
+ public final static String PROP_BUILD_VERSION = "ro.build.version.release";
+ public final static String PROP_BUILD_VERSION_NUMBER = "ro.build.version.sdk";
+ public final static String PROP_DEBUGGABLE = "ro.debuggable";
+ /** Serial number of the first connected emulator. */
+ public final static String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$
+ /** Device change bit mask: {@link DeviceState} change. */
+ public static final int CHANGE_STATE = 0x0001;
+ /** Device change bit mask: {@link Client} list change. */
+ public static final int CHANGE_CLIENT_LIST = 0x0002;
+ /** Device change bit mask: build info change. */
+ public static final int CHANGE_BUILD_INFO = 0x0004;
+
+ /**
+ * Returns the serial number of the device.
+ */
+ public String getSerialNumber();
+
+ /**
+ * Returns the name of the AVD the emulator is running.
+ * <p/>This is only valid if {@link #isEmulator()} returns true.
+ * <p/>If the emulator is not running any AVD (for instance it's running from an Android source
+ * tree build), this method will return "<code>&lt;build&gt;</code>".
+ * @return the name of the AVD or <code>null</code> if there isn't any.
+ */
+ public String getAvdName();
+
+ /**
+ * Returns the state of the device.
+ */
+ public DeviceState getState();
+
+ /**
+ * Returns the device properties. It contains the whole output of 'getprop'
+ */
+ public Map<String, String> getProperties();
+
+ /**
+ * Returns the number of property for this device.
+ */
+ public int getPropertyCount();
+
+ /**
+ * Returns a property value.
+ * @param name the name of the value to return.
+ * @return the value or <code>null</code> if the property does not exist.
+ */
+ public String getProperty(String name);
+
+ /**
+ * Returns if the device is ready.
+ * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#ONLINE}.
+ */
+ public boolean isOnline();
+
+ /**
+ * Returns <code>true</code> if the device is an emulator.
+ */
+ public boolean isEmulator();
+
+ /**
+ * Returns if the device is offline.
+ * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#OFFLINE}.
+ */
+ public boolean isOffline();
+
+ /**
+ * Returns if the device is in bootloader mode.
+ * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#BOOTLOADER}.
+ */
+ public boolean isBootLoader();
+
+ /**
+ * Returns whether the {@link Device} has {@link Client}s.
+ */
+ public boolean hasClients();
+
+ /**
+ * Returns the array of clients.
+ */
+ public Client[] getClients();
+
+ /**
+ * Returns a {@link Client} by its application name.
+ * @param applicationName the name of the application
+ * @return the <code>Client</code> object or <code>null</code> if no match was found.
+ */
+ public Client getClient(String applicationName);
+
+ /**
+ * Returns a {@link SyncService} object to push / pull files to and from the device.
+ * @return <code>null</code> if the SyncService couldn't be created.
+ */
+ public SyncService getSyncService();
+
+ /**
+ * Returns a {@link FileListingService} for this device.
+ */
+ public FileListingService getFileListingService();
+
+ /**
+ * Takes a screen shot of the device and returns it as a {@link RawImage}.
+ * @return the screenshot as a <code>RawImage</code> or <code>null</code> if
+ * something went wrong.
+ * @throws IOException
+ */
+ public RawImage getScreenshot() throws IOException;
+
+ /**
+ * Executes a shell command on the device, and sends the result to a receiver.
+ * @param command The command to execute
+ * @param receiver The receiver object getting the result from the command.
+ * @throws IOException
+ */
+ public void executeShellCommand(String command,
+ IShellOutputReceiver receiver) throws IOException;
+
+ /**
+ * Runs the event log service and outputs the event log to the {@link LogReceiver}.
+ * @param receiver the receiver to receive the event log entries.
+ * @throws IOException
+ */
+ public void runEventLogService(LogReceiver receiver) throws IOException;
+
+ /**
+ * Runs the log service for the given log and outputs the log to the {@link LogReceiver}.
+ * @param logname the logname of the log to read from.
+ * @param receiver the receiver to receive the event log entries.
+ * @throws IOException
+ */
+ public void runLogService(String logname, LogReceiver receiver) throws IOException;
+
+ /**
+ * Creates a port forwarding between a local and a remote port.
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ */
+ public boolean createForward(int localPort, int remotePort);
+
+ /**
+ * Removes a port forwarding between a local and a remote port.
+ * @param localPort the local port to forward
+ * @param remotePort the remote port.
+ * @return <code>true</code> if success.
+ */
+ public boolean removeForward(int localPort, int remotePort);
+
+ /**
+ * Returns the name of the client by pid or <code>null</code> if pid is unknown
+ * @param pid the pid of the client.
+ */
+ public String getClientName(int pid);
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java
new file mode 100644
index 0000000..fb671bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Classes which implement this interface provide methods that deal with out from a remote shell
+ * command on a device/emulator.
+ */
+public interface IShellOutputReceiver {
+ /**
+ * Called every time some new data is available.
+ * @param data The new data.
+ * @param offset The offset at which the new data starts.
+ * @param length The length of the new data.
+ */
+ public void addOutput(byte[] data, int offset, int length);
+
+ /**
+ * Called at the end of the process execution (unless the process was
+ * canceled). This allows the receiver to terminate and flush whatever
+ * data was not yet processed.
+ */
+ public void flush();
+
+ /**
+ * Cancel method to stop the execution of the remote shell command.
+ * @return true to cancel the execution of the command.
+ */
+ public boolean isCancelled();
+};
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java
new file mode 100644
index 0000000..3b9d730
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Classes which implement this interface provide a method that returns a stack trace.
+ */
+public interface IStackTraceInfo {
+
+ /**
+ * Returns the stack trace. This can be <code>null</code>.
+ */
+ public StackTraceElement[] getStackTrace();
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
new file mode 100644
index 0000000..92bbb82
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
@@ -0,0 +1,371 @@
+/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package com.android.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SocketChannel;
+
+/**
+ * A JDWP packet, sitting at the start of a ByteBuffer somewhere.
+ *
+ * This allows us to wrap a "pointer" to the data with the results of
+ * decoding the packet.
+ *
+ * None of the operations here are synchronized. If multiple threads will
+ * be accessing the same ByteBuffers, external sync will be required.
+ *
+ * Use the constructor to create an empty packet, or "findPacket()" to
+ * wrap a JdwpPacket around existing data.
+ */
+final class JdwpPacket {
+ // header len
+ public static final int JDWP_HEADER_LEN = 11;
+
+ // results from findHandshake
+ public static final int HANDSHAKE_GOOD = 1;
+ public static final int HANDSHAKE_NOTYET = 2;
+ public static final int HANDSHAKE_BAD = 3;
+
+ // our cmdSet/cmd
+ private static final int DDMS_CMD_SET = 0xc7; // 'G' + 128
+ private static final int DDMS_CMD = 0x01;
+
+ // "flags" field
+ private static final int REPLY_PACKET = 0x80;
+
+ // this is sent and expected at the start of a JDWP connection
+ private static final byte[] mHandshake = {
+ 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'
+ };
+
+ public static final int HANDSHAKE_LEN = mHandshake.length;
+
+ private ByteBuffer mBuffer;
+ private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode;
+ private boolean mIsNew;
+
+ private static int mSerialId = 0x40000000;
+
+
+ /**
+ * Create a new, empty packet, in "buf".
+ */
+ JdwpPacket(ByteBuffer buf) {
+ mBuffer = buf;
+ mIsNew = true;
+ }
+
+ /**
+ * Finish a packet created with newPacket().
+ *
+ * This always creates a command packet, with the next serial number
+ * in sequence.
+ *
+ * We have to take "payloadLength" as an argument because we can't
+ * see the position in the "slice" returned by getPayload(). We could
+ * fish it out of the chunk header, but it's legal for there to be
+ * more than one chunk in a JDWP packet.
+ *
+ * On exit, "position" points to the end of the data.
+ */
+ void finishPacket(int payloadLength) {
+ assert mIsNew;
+
+ ByteOrder oldOrder = mBuffer.order();
+ mBuffer.order(ChunkHandler.CHUNK_ORDER);
+
+ mLength = JDWP_HEADER_LEN + payloadLength;
+ mId = getNextSerial();
+ mFlags = 0;
+ mCmdSet = DDMS_CMD_SET;
+ mCmd = DDMS_CMD;
+
+ mBuffer.putInt(0x00, mLength);
+ mBuffer.putInt(0x04, mId);
+ mBuffer.put(0x08, (byte) mFlags);
+ mBuffer.put(0x09, (byte) mCmdSet);
+ mBuffer.put(0x0a, (byte) mCmd);
+
+ mBuffer.order(oldOrder);
+ mBuffer.position(mLength);
+ }
+
+ /**
+ * Get the next serial number. This creates a unique serial number
+ * across all connections, not just for the current connection. This
+ * is a useful property when debugging, but isn't necessary.
+ *
+ * We can't synchronize on an int, so we use a sync method.
+ */
+ private static synchronized int getNextSerial() {
+ return mSerialId++;
+ }
+
+ /**
+ * Return a slice of the byte buffer, positioned past the JDWP header
+ * to the start of the chunk header. The buffer's limit will be set
+ * to the size of the payload if the size is known; if this is a
+ * packet under construction the limit will be set to the end of the
+ * buffer.
+ *
+ * Doesn't examine the packet at all -- works on empty buffers.
+ */
+ ByteBuffer getPayload() {
+ ByteBuffer buf;
+ int oldPosn = mBuffer.position();
+
+ mBuffer.position(JDWP_HEADER_LEN);
+ buf = mBuffer.slice(); // goes from position to limit
+ mBuffer.position(oldPosn);
+
+ if (mLength > 0)
+ buf.limit(mLength - JDWP_HEADER_LEN);
+ else
+ assert mIsNew;
+ buf.order(ChunkHandler.CHUNK_ORDER);
+ return buf;
+ }
+
+ /**
+ * Returns "true" if this JDWP packet has a JDWP command type.
+ *
+ * This never returns "true" for reply packets.
+ */
+ boolean isDdmPacket() {
+ return (mFlags & REPLY_PACKET) == 0 &&
+ mCmdSet == DDMS_CMD_SET &&
+ mCmd == DDMS_CMD;
+ }
+
+ /**
+ * Returns "true" if this JDWP packet is tagged as a reply.
+ */
+ boolean isReply() {
+ return (mFlags & REPLY_PACKET) != 0;
+ }
+
+ /**
+ * Returns "true" if this JDWP packet is a reply with a nonzero
+ * error code.
+ */
+ boolean isError() {
+ return isReply() && mErrCode != 0;
+ }
+
+ /**
+ * Returns "true" if this JDWP packet has no data.
+ */
+ boolean isEmpty() {
+ return (mLength == JDWP_HEADER_LEN);
+ }
+
+ /**
+ * Return the packet's ID. For a reply packet, this allows us to
+ * match the reply with the original request.
+ */
+ int getId() {
+ return mId;
+ }
+
+ /**
+ * Return the length of a packet. This includes the header, so an
+ * empty packet is 11 bytes long.
+ */
+ int getLength() {
+ return mLength;
+ }
+
+ /**
+ * Write our packet to "chan". Consumes the packet as part of the
+ * write.
+ *
+ * The JDWP packet starts at offset 0 and ends at mBuffer.position().
+ */
+ void writeAndConsume(SocketChannel chan) throws IOException {
+ int oldLimit;
+
+ //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position()
+ // + ", limit=" + mBuffer.limit());
+
+ assert mLength > 0;
+
+ mBuffer.flip(); // limit<-posn, posn<-0
+ oldLimit = mBuffer.limit();
+ mBuffer.limit(mLength);
+ while (mBuffer.position() != mBuffer.limit()) {
+ chan.write(mBuffer);
+ }
+ // position should now be at end of packet
+ assert mBuffer.position() == mLength;
+
+ mBuffer.limit(oldLimit);
+ mBuffer.compact(); // shift posn...limit, posn<-pending data
+
+ //Log.i("ddms", " : pos=" + mBuffer.position()
+ // + ", limit=" + mBuffer.limit());
+ }
+
+ /**
+ * "Move" the packet data out of the buffer we're sitting on and into
+ * buf at the current position.
+ */
+ void movePacket(ByteBuffer buf) {
+ Log.v("ddms", "moving " + mLength + " bytes");
+ int oldPosn = mBuffer.position();
+
+ mBuffer.position(0);
+ mBuffer.limit(mLength);
+ buf.put(mBuffer);
+ mBuffer.position(mLength);
+ mBuffer.limit(oldPosn);
+ mBuffer.compact(); // shift posn...limit, posn<-pending data
+ }
+
+ /**
+ * Consume the JDWP packet.
+ *
+ * On entry and exit, "position" is the #of bytes in the buffer.
+ */
+ void consume()
+ {
+ //Log.d("ddms", "consuming " + mLength + " bytes");
+ //Log.d("ddms", " posn=" + mBuffer.position()
+ // + ", limit=" + mBuffer.limit());
+
+ /*
+ * The "flip" call sets "limit" equal to the position (usually the
+ * end of data) and "position" equal to zero.
+ *
+ * compact() copies everything from "position" and "limit" to the
+ * start of the buffer, sets "position" to the end of data, and
+ * sets "limit" to the capacity.
+ *
+ * On entry, "position" is set to the amount of data in the buffer
+ * and "limit" is set to the capacity. We want to call flip()
+ * so that position..limit spans our data, advance "position" past
+ * the current packet, then compact.
+ */
+ mBuffer.flip(); // limit<-posn, posn<-0
+ mBuffer.position(mLength);
+ mBuffer.compact(); // shift posn...limit, posn<-pending data
+ mLength = 0;
+ //Log.d("ddms", " after compact, posn=" + mBuffer.position()
+ // + ", limit=" + mBuffer.limit());
+ }
+
+ /**
+ * Find the JDWP packet at the start of "buf". The start is known,
+ * but the length has to be parsed out.
+ *
+ * On entry, the packet data in "buf" must start at offset 0 and end
+ * at "position". "limit" should be set to the buffer capacity. This
+ * method does not alter "buf"s attributes.
+ *
+ * Returns a new JdwpPacket if a full one is found in the buffer. If
+ * not, returns null. Throws an exception if the data doesn't look like
+ * a valid JDWP packet.
+ */
+ static JdwpPacket findPacket(ByteBuffer buf) {
+ int count = buf.position();
+ int length, id, flags, cmdSet, cmd;
+
+ if (count < JDWP_HEADER_LEN)
+ return null;
+
+ ByteOrder oldOrder = buf.order();
+ buf.order(ChunkHandler.CHUNK_ORDER);
+
+ length = buf.getInt(0x00);
+ id = buf.getInt(0x04);
+ flags = buf.get(0x08) & 0xff;
+ cmdSet = buf.get(0x09) & 0xff;
+ cmd = buf.get(0x0a) & 0xff;
+
+ buf.order(oldOrder);
+
+ if (length < JDWP_HEADER_LEN)
+ throw new BadPacketException();
+ if (count < length)
+ return null;
+
+ JdwpPacket pkt = new JdwpPacket(buf);
+ //pkt.mBuffer = buf;
+ pkt.mLength = length;
+ pkt.mId = id;
+ pkt.mFlags = flags;
+
+ if ((flags & REPLY_PACKET) == 0) {
+ pkt.mCmdSet = cmdSet;
+ pkt.mCmd = cmd;
+ pkt.mErrCode = -1;
+ } else {
+ pkt.mCmdSet = -1;
+ pkt.mCmd = -1;
+ pkt.mErrCode = cmdSet | (cmd << 8);
+ }
+
+ return pkt;
+ }
+
+ /**
+ * Like findPacket(), but when we're expecting the JDWP handshake.
+ *
+ * Returns one of:
+ * HANDSHAKE_GOOD - found handshake, looks good
+ * HANDSHAKE_BAD - found enough data, but it's wrong
+ * HANDSHAKE_NOTYET - not enough data has been read yet
+ */
+ static int findHandshake(ByteBuffer buf) {
+ int count = buf.position();
+ int i;
+
+ if (count < mHandshake.length)
+ return HANDSHAKE_NOTYET;
+
+ for (i = mHandshake.length -1; i >= 0; --i) {
+ if (buf.get(i) != mHandshake[i])
+ return HANDSHAKE_BAD;
+ }
+
+ return HANDSHAKE_GOOD;
+ }
+
+ /**
+ * Remove the handshake string from the buffer.
+ *
+ * On entry and exit, "position" is the #of bytes in the buffer.
+ */
+ static void consumeHandshake(ByteBuffer buf) {
+ // in theory, nothing else can have arrived, so this is overkill
+ buf.flip(); // limit<-posn, posn<-0
+ buf.position(mHandshake.length);
+ buf.compact(); // shift posn...limit, posn<-pending data
+ }
+
+ /**
+ * Copy the handshake string into the output buffer.
+ *
+ * On exit, "buf"s position will be advanced.
+ */
+ static void putHandshake(ByteBuffer buf) {
+ buf.put(mHandshake);
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java
new file mode 100644
index 0000000..ce95b04
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Log class that mirrors the API in main Android sources.
+ * <p/>Default behavior outputs the log to {@link System#out}. Use
+ * {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else.
+ */
+public final class Log {
+
+ /**
+ * Log Level enum.
+ */
+ public enum LogLevel {
+ VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$
+ DEBUG(3, "debug", 'D'), //$NON-NLS-1$
+ INFO(4, "info", 'I'), //$NON-NLS-1$
+ WARN(5, "warn", 'W'), //$NON-NLS-1$
+ ERROR(6, "error", 'E'), //$NON-NLS-1$
+ ASSERT(7, "assert", 'A'); //$NON-NLS-1$
+
+ private int mPriorityLevel;
+ private String mStringValue;
+ private char mPriorityLetter;
+
+ LogLevel(int intPriority, String stringValue, char priorityChar) {
+ mPriorityLevel = intPriority;
+ mStringValue = stringValue;
+ mPriorityLetter = priorityChar;
+ }
+
+ public static LogLevel getByString(String value) {
+ for (LogLevel mode : values()) {
+ if (mode.mStringValue.equals(value)) {
+ return mode;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link LogLevel} enum matching the specified letter.
+ * @param letter the letter matching a <code>LogLevel</code> enum
+ * @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
+ */
+ public static LogLevel getByLetter(char letter) {
+ for (LogLevel mode : values()) {
+ if (mode.mPriorityLetter == letter) {
+ return mode;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link LogLevel} enum matching the specified letter.
+ * <p/>
+ * The letter is passed as a {@link String} argument, but only the first character
+ * is used.
+ * @param letter the letter matching a <code>LogLevel</code> enum
+ * @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
+ */
+ public static LogLevel getByLetterString(String letter) {
+ if (letter.length() > 0) {
+ return getByLetter(letter.charAt(0));
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the letter identifying the priority of the {@link LogLevel}.
+ */
+ public char getPriorityLetter() {
+ return mPriorityLetter;
+ }
+
+ /**
+ * Returns the numerical value of the priority.
+ */
+ public int getPriority() {
+ return mPriorityLevel;
+ }
+
+ /**
+ * Returns a non translated string representing the LogLevel.
+ */
+ public String getStringValue() {
+ return mStringValue;
+ }
+ }
+
+ /**
+ * Classes which implement this interface provides methods that deal with outputting log
+ * messages.
+ */
+ public interface ILogOutput {
+ /**
+ * Sent when a log message needs to be printed.
+ * @param logLevel The {@link LogLevel} enum representing the priority of the message.
+ * @param tag The tag associated with the message.
+ * @param message The message to display.
+ */
+ public void printLog(LogLevel logLevel, String tag, String message);
+
+ /**
+ * Sent when a log message needs to be printed, and, if possible, displayed to the user
+ * in a dialog box.
+ * @param logLevel The {@link LogLevel} enum representing the priority of the message.
+ * @param tag The tag associated with the message.
+ * @param message The message to display.
+ */
+ public void printAndPromptLog(LogLevel logLevel, String tag, String message);
+ }
+
+ private static LogLevel mLevel = DdmPreferences.getLogLevel();
+
+ private static ILogOutput sLogOutput;
+
+ private static final char[] mSpaceLine = new char[72];
+ private static final char[] mHexDigit = new char[]
+ { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+ static {
+ /* prep for hex dump */
+ int i = mSpaceLine.length-1;
+ while (i >= 0)
+ mSpaceLine[i--] = ' ';
+ mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0';
+ mSpaceLine[4] = '-';
+ }
+
+ static final class Config {
+ static final boolean LOGV = true;
+ static final boolean LOGD = true;
+ };
+
+ private Log() {}
+
+ /**
+ * Outputs a {@link LogLevel#VERBOSE} level message.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void v(String tag, String message) {
+ println(LogLevel.VERBOSE, tag, message);
+ }
+
+ /**
+ * Outputs a {@link LogLevel#DEBUG} level message.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void d(String tag, String message) {
+ println(LogLevel.DEBUG, tag, message);
+ }
+
+ /**
+ * Outputs a {@link LogLevel#INFO} level message.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void i(String tag, String message) {
+ println(LogLevel.INFO, tag, message);
+ }
+
+ /**
+ * Outputs a {@link LogLevel#WARN} level message.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void w(String tag, String message) {
+ println(LogLevel.WARN, tag, message);
+ }
+
+ /**
+ * Outputs a {@link LogLevel#ERROR} level message.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void e(String tag, String message) {
+ println(LogLevel.ERROR, tag, message);
+ }
+
+ /**
+ * Outputs a log message and attempts to display it in a dialog.
+ * @param tag The tag associated with the message.
+ * @param message The message to output.
+ */
+ public static void logAndDisplay(LogLevel logLevel, String tag, String message) {
+ if (sLogOutput != null) {
+ sLogOutput.printAndPromptLog(logLevel, tag, message);
+ } else {
+ println(logLevel, tag, message);
+ }
+ }
+
+ /**
+ * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information.
+ * @param tag The tag associated with the message.
+ * @param throwable The {@link Throwable} to output.
+ */
+ public static void e(String tag, Throwable throwable) {
+ if (throwable != null) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+
+ throwable.printStackTrace(pw);
+ println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString());
+ }
+ }
+
+ static void setLevel(LogLevel logLevel) {
+ mLevel = logLevel;
+ }
+
+ /**
+ * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out}
+ * will be used.
+ * @param logOutput The {@link ILogOutput} to use to print the log.
+ */
+ public static void setLogOutput(ILogOutput logOutput) {
+ sLogOutput = logOutput;
+ }
+
+ /**
+ * Show hex dump.
+ * <p/>
+ * Local addition. Output looks like:
+ * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef
+ * <p/>
+ * Uses no string concatenation; creates one String object per line.
+ */
+ static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) {
+
+ int kHexOffset = 6;
+ int kAscOffset = 55;
+ char[] line = new char[mSpaceLine.length];
+ int addr, baseAddr, count;
+ int i, ch;
+ boolean needErase = true;
+
+ //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length);
+
+ baseAddr = 0;
+ while (length != 0) {
+ if (length > 16) {
+ // full line
+ count = 16;
+ } else {
+ // partial line; re-copy blanks to clear end
+ count = length;
+ needErase = true;
+ }
+
+ if (needErase) {
+ System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length);
+ needErase = false;
+ }
+
+ // output the address (currently limited to 4 hex digits)
+ addr = baseAddr;
+ addr &= 0xffff;
+ ch = 3;
+ while (addr != 0) {
+ line[ch] = mHexDigit[addr & 0x0f];
+ ch--;
+ addr >>>= 4;
+ }
+
+ // output hex digits and ASCII chars
+ ch = kHexOffset;
+ for (i = 0; i < count; i++) {
+ byte val = data[offset + i];
+
+ line[ch++] = mHexDigit[(val >>> 4) & 0x0f];
+ line[ch++] = mHexDigit[val & 0x0f];
+ ch++;
+
+ if (val >= 0x20 && val < 0x7f)
+ line[kAscOffset + i] = (char) val;
+ else
+ line[kAscOffset + i] = '.';
+ }
+
+ println(level, tag, new String(line));
+
+ // advance to next chunk of data
+ length -= count;
+ offset += count;
+ baseAddr += count;
+ }
+
+ }
+
+ /**
+ * Dump the entire contents of a byte array with DEBUG priority.
+ */
+ static void hexDump(byte[] data) {
+ hexDump("ddms", LogLevel.DEBUG, data, 0, data.length);
+ }
+
+ /* currently prints to stdout; could write to a log window */
+ private static void println(LogLevel logLevel, String tag, String message) {
+ if (logLevel.getPriority() >= mLevel.getPriority()) {
+ if (sLogOutput != null) {
+ sLogOutput.printLog(logLevel, tag, message);
+ } else {
+ printLog(logLevel, tag, message);
+ }
+ }
+ }
+
+ /**
+ * Prints a log message.
+ * @param logLevel
+ * @param tag
+ * @param message
+ */
+ public static void printLog(LogLevel logLevel, String tag, String message) {
+ long msec;
+
+ msec = System.currentTimeMillis();
+ String outMessage = String.format("%02d:%02d %c/%s: %s\n",
+ (msec / 60000) % 60, (msec / 1000) % 60,
+ logLevel.getPriorityLetter(), tag, message);
+ System.out.print(outMessage);
+ }
+
+}
+
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java
new file mode 100644
index 0000000..79eb5bb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java
@@ -0,0 +1,780 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.NotYetBoundException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Monitor open connections.
+ */
+final class MonitorThread extends Thread {
+
+ // For broadcasts to message handlers
+ //private static final int CLIENT_CONNECTED = 1;
+
+ private static final int CLIENT_READY = 2;
+
+ private static final int CLIENT_DISCONNECTED = 3;
+
+ private volatile boolean mQuit = false;
+
+ // List of clients we're paying attention to
+ private ArrayList<Client> mClientList;
+
+ // The almighty mux
+ private Selector mSelector;
+
+ // Map chunk types to handlers
+ private HashMap<Integer, ChunkHandler> mHandlerMap;
+
+ // port for "debug selected"
+ private ServerSocketChannel mDebugSelectedChan;
+
+ private int mNewDebugSelectedPort;
+
+ private int mDebugSelectedPort = -1;
+
+ /**
+ * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port.
+ */
+ private Client mSelectedClient = null;
+
+ // singleton
+ private static MonitorThread mInstance;
+
+ /**
+ * Generic constructor.
+ */
+ private MonitorThread() {
+ super("Monitor");
+ mClientList = new ArrayList<Client>();
+ mHandlerMap = new HashMap<Integer, ChunkHandler>();
+
+ mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort();
+ }
+
+ /**
+ * Creates and return the singleton instance of the client monitor thread.
+ */
+ static MonitorThread createInstance() {
+ return mInstance = new MonitorThread();
+ }
+
+ /**
+ * Get singleton instance of the client monitor thread.
+ */
+ static MonitorThread getInstance() {
+ return mInstance;
+ }
+
+
+ /**
+ * Sets or changes the port number for "debug selected".
+ */
+ synchronized void setDebugSelectedPort(int port) throws IllegalStateException {
+ if (mInstance == null) {
+ return;
+ }
+
+ if (AndroidDebugBridge.getClientSupport() == false) {
+ return;
+ }
+
+ if (mDebugSelectedChan != null) {
+ Log.d("ddms", "Changing debug-selected port to " + port);
+ mNewDebugSelectedPort = port;
+ wakeup();
+ } else {
+ // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically
+ // opened on the first run loop.
+ mNewDebugSelectedPort = port;
+ }
+ }
+
+ /**
+ * Sets the client to accept debugger connection on the custom "Selected debug port".
+ * @param selectedClient the client. Can be null.
+ */
+ synchronized void setSelectedClient(Client selectedClient) {
+ if (mInstance == null) {
+ return;
+ }
+
+ if (mSelectedClient != selectedClient) {
+ Client oldClient = mSelectedClient;
+ mSelectedClient = selectedClient;
+
+ if (oldClient != null) {
+ oldClient.update(Client.CHANGE_PORT);
+ }
+
+ if (mSelectedClient != null) {
+ mSelectedClient.update(Client.CHANGE_PORT);
+ }
+ }
+ }
+
+ /**
+ * Returns the client accepting debugger connection on the custom "Selected debug port".
+ */
+ Client getSelectedClient() {
+ return mSelectedClient;
+ }
+
+
+ /**
+ * Returns "true" if we want to retry connections to clients if we get a bad
+ * JDWP handshake back, "false" if we want to just mark them as bad and
+ * leave them alone.
+ */
+ boolean getRetryOnBadHandshake() {
+ return true; // TODO? make configurable
+ }
+
+ /**
+ * Get an array of known clients.
+ */
+ Client[] getClients() {
+ synchronized (mClientList) {
+ return mClientList.toArray(new Client[0]);
+ }
+ }
+
+ /**
+ * Register "handler" as the handler for type "type".
+ */
+ synchronized void registerChunkHandler(int type, ChunkHandler handler) {
+ if (mInstance == null) {
+ return;
+ }
+
+ synchronized (mHandlerMap) {
+ if (mHandlerMap.get(type) == null) {
+ mHandlerMap.put(type, handler);
+ }
+ }
+ }
+
+ /**
+ * Watch for activity from clients and debuggers.
+ */
+ @Override
+ public void run() {
+ Log.d("ddms", "Monitor is up");
+
+ // create a selector
+ try {
+ mSelector = Selector.open();
+ } catch (IOException ioe) {
+ Log.logAndDisplay(LogLevel.ERROR, "ddms",
+ "Failed to initialize Monitor Thread: " + ioe.getMessage());
+ return;
+ }
+
+ while (!mQuit) {
+
+ try {
+ /*
+ * sync with new registrations: we wait until addClient is done before going through
+ * and doing mSelector.select() again.
+ * @see {@link #addClient(Client)}
+ */
+ synchronized (mClientList) {
+ }
+
+ // (re-)open the "debug selected" port, if it's not opened yet or
+ // if the port changed.
+ try {
+ if (AndroidDebugBridge.getClientSupport()) {
+ if ((mDebugSelectedChan == null ||
+ mNewDebugSelectedPort != mDebugSelectedPort) &&
+ mNewDebugSelectedPort != -1) {
+ if (reopenDebugSelectedPort()) {
+ mDebugSelectedPort = mNewDebugSelectedPort;
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ Log.e("ddms",
+ "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort);
+ Log.e("ddms", ioe);
+ mNewDebugSelectedPort = mDebugSelectedPort; // no retry
+ }
+
+ int count;
+ try {
+ count = mSelector.select();
+ } catch (IOException ioe) {
+ ioe.printStackTrace();
+ continue;
+ } catch (CancelledKeyException cke) {
+ continue;
+ }
+
+ if (count == 0) {
+ // somebody called wakeup() ?
+ // Log.i("ddms", "selector looping");
+ continue;
+ }
+
+ Set<SelectionKey> keys = mSelector.selectedKeys();
+ Iterator<SelectionKey> iter = keys.iterator();
+
+ while (iter.hasNext()) {
+ SelectionKey key = iter.next();
+ iter.remove();
+
+ try {
+ if (key.attachment() instanceof Client) {
+ processClientActivity(key);
+ }
+ else if (key.attachment() instanceof Debugger) {
+ processDebuggerActivity(key);
+ }
+ else if (key.attachment() instanceof MonitorThread) {
+ processDebugSelectedActivity(key);
+ }
+ else {
+ Log.e("ddms", "unknown activity key");
+ }
+ } catch (Exception e) {
+ // we don't want to have our thread be killed because of any uncaught
+ // exception, so we intercept all here.
+ Log.e("ddms", "Exception during activity from Selector.");
+ Log.e("ddms", e);
+ }
+ }
+ } catch (Exception e) {
+ // we don't want to have our thread be killed because of any uncaught
+ // exception, so we intercept all here.
+ Log.e("ddms", "Exception MonitorThread.run()");
+ Log.e("ddms", e);
+ }
+ }
+ }
+
+
+ /**
+ * Returns the port on which the selected client listen for debugger
+ */
+ int getDebugSelectedPort() {
+ return mDebugSelectedPort;
+ }
+
+ /*
+ * Something happened. Figure out what.
+ */
+ private void processClientActivity(SelectionKey key) {
+ Client client = (Client)key.attachment();
+
+ try {
+ if (key.isReadable() == false || key.isValid() == false) {
+ Log.d("ddms", "Invalid key from " + client + ". Dropping client.");
+ dropClient(client, true /* notify */);
+ return;
+ }
+
+ client.read();
+
+ /*
+ * See if we have a full packet in the buffer. It's possible we have
+ * more than one packet, so we have to loop.
+ */
+ JdwpPacket packet = client.getJdwpPacket();
+ while (packet != null) {
+ if (packet.isDdmPacket()) {
+ // unsolicited DDM request - hand it off
+ assert !packet.isReply();
+ callHandler(client, packet, null);
+ packet.consume();
+ } else if (packet.isReply()
+ && client.isResponseToUs(packet.getId()) != null) {
+ // reply to earlier DDM request
+ ChunkHandler handler = client
+ .isResponseToUs(packet.getId());
+ if (packet.isError())
+ client.packetFailed(packet);
+ else if (packet.isEmpty())
+ Log.d("ddms", "Got empty reply for 0x"
+ + Integer.toHexString(packet.getId())
+ + " from " + client);
+ else
+ callHandler(client, packet, handler);
+ packet.consume();
+ client.removeRequestId(packet.getId());
+ } else {
+ Log.v("ddms", "Forwarding client "
+ + (packet.isReply() ? "reply" : "event") + " 0x"
+ + Integer.toHexString(packet.getId()) + " to "
+ + client.getDebugger());
+ client.forwardPacketToDebugger(packet);
+ }
+
+ // find next
+ packet = client.getJdwpPacket();
+ }
+ } catch (CancelledKeyException e) {
+ // key was canceled probably due to a disconnected client before we could
+ // read stuff coming from the client, so we drop it.
+ dropClient(client, true /* notify */);
+ } catch (IOException ex) {
+ // something closed down, no need to print anything. The client is simply dropped.
+ dropClient(client, true /* notify */);
+ } catch (Exception ex) {
+ Log.e("ddms", ex);
+
+ /* close the client; automatically un-registers from selector */
+ dropClient(client, true /* notify */);
+
+ if (ex instanceof BufferOverflowException) {
+ Log.w("ddms",
+ "Client data packet exceeded maximum buffer size "
+ + client);
+ } else {
+ // don't know what this is, display it
+ Log.e("ddms", ex);
+ }
+ }
+ }
+
+ /*
+ * Process an incoming DDM packet. If this is a reply to an earlier request,
+ * "handler" will be set to the handler responsible for the original
+ * request. The spec allows a JDWP message to include multiple DDM chunks.
+ */
+ private void callHandler(Client client, JdwpPacket packet,
+ ChunkHandler handler) {
+
+ // on first DDM packet received, broadcast a "ready" message
+ if (!client.ddmSeen())
+ broadcast(CLIENT_READY, client);
+
+ ByteBuffer buf = packet.getPayload();
+ int type, length;
+ boolean reply = true;
+
+ type = buf.getInt();
+ length = buf.getInt();
+
+ if (handler == null) {
+ // not a reply, figure out who wants it
+ synchronized (mHandlerMap) {
+ handler = mHandlerMap.get(type);
+ reply = false;
+ }
+ }
+
+ if (handler == null) {
+ Log.w("ddms", "Received unsupported chunk type "
+ + ChunkHandler.name(type) + " (len=" + length + ")");
+ } else {
+ Log.d("ddms", "Calling handler for " + ChunkHandler.name(type)
+ + " [" + handler + "] (len=" + length + ")");
+ ByteBuffer ibuf = buf.slice();
+ ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O
+ roBuf.order(ChunkHandler.CHUNK_ORDER);
+ // do the handling of the chunk synchronized on the client list
+ // to be sure there's no concurrency issue when we look for HOME
+ // in hasApp()
+ synchronized (mClientList) {
+ handler.handleChunk(client, type, roBuf, reply, packet.getId());
+ }
+ }
+ }
+
+ /**
+ * Drops a client from the monitor.
+ * <p/>This will lock the {@link Client} list of the {@link Device} running <var>client</var>.
+ * @param client
+ * @param notify
+ */
+ synchronized void dropClient(Client client, boolean notify) {
+ if (mInstance == null) {
+ return;
+ }
+
+ synchronized (mClientList) {
+ if (mClientList.remove(client) == false) {
+ return;
+ }
+ }
+ client.close(notify);
+ broadcast(CLIENT_DISCONNECTED, client);
+
+ /*
+ * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0
+ * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504
+ */
+ wakeup();
+ }
+
+ /*
+ * Process activity from one of the debugger sockets. This could be a new
+ * connection or a data packet.
+ */
+ private void processDebuggerActivity(SelectionKey key) {
+ Debugger dbg = (Debugger)key.attachment();
+
+ try {
+ if (key.isAcceptable()) {
+ try {
+ acceptNewDebugger(dbg, null);
+ } catch (IOException ioe) {
+ Log.w("ddms", "debugger accept() failed");
+ ioe.printStackTrace();
+ }
+ } else if (key.isReadable()) {
+ processDebuggerData(key);
+ } else {
+ Log.d("ddm-debugger", "key in unknown state");
+ }
+ } catch (CancelledKeyException cke) {
+ // key has been cancelled we can ignore that.
+ }
+ }
+
+ /*
+ * Accept a new connection from a debugger. If successful, register it with
+ * the Selector.
+ */
+ private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan)
+ throws IOException {
+
+ synchronized (mClientList) {
+ SocketChannel chan;
+
+ if (acceptChan == null)
+ chan = dbg.accept();
+ else
+ chan = dbg.accept(acceptChan);
+
+ if (chan != null) {
+ chan.socket().setTcpNoDelay(true);
+
+ wakeup();
+
+ try {
+ chan.register(mSelector, SelectionKey.OP_READ, dbg);
+ } catch (IOException ioe) {
+ // failed, drop the connection
+ dbg.closeData();
+ throw ioe;
+ } catch (RuntimeException re) {
+ // failed, drop the connection
+ dbg.closeData();
+ throw re;
+ }
+ } else {
+ Log.i("ddms", "ignoring duplicate debugger");
+ // new connection already closed
+ }
+ }
+ }
+
+ /*
+ * We have incoming data from the debugger. Forward it to the client.
+ */
+ private void processDebuggerData(SelectionKey key) {
+ Debugger dbg = (Debugger)key.attachment();
+
+ try {
+ /*
+ * Read pending data.
+ */
+ dbg.read();
+
+ /*
+ * See if we have a full packet in the buffer. It's possible we have
+ * more than one packet, so we have to loop.
+ */
+ JdwpPacket packet = dbg.getJdwpPacket();
+ while (packet != null) {
+ Log.v("ddms", "Forwarding dbg req 0x"
+ + Integer.toHexString(packet.getId()) + " to "
+ + dbg.getClient());
+
+ dbg.forwardPacketToClient(packet);
+
+ packet = dbg.getJdwpPacket();
+ }
+ } catch (IOException ioe) {
+ /*
+ * Close data connection; automatically un-registers dbg from
+ * selector. The failure could be caused by the debugger going away,
+ * or by the client going away and failing to accept our data.
+ * Either way, the debugger connection does not need to exist any
+ * longer. We also need to recycle the connection to the client, so
+ * that the VM sees the debugger disconnect. For a DDM-aware client
+ * this won't be necessary, and we can just send a "debugger
+ * disconnected" message.
+ */
+ Log.i("ddms", "Closing connection to debugger " + dbg);
+ dbg.closeData();
+ Client client = dbg.getClient();
+ if (client.isDdmAware()) {
+ // TODO: soft-disconnect DDM-aware clients
+ Log.i("ddms", " (recycling client connection as well)");
+
+ // we should drop the client, but also attempt to reopen it.
+ // This is done by the DeviceMonitor.
+ client.getDevice().getMonitor().addClientToDropAndReopen(client,
+ IDebugPortProvider.NO_STATIC_PORT);
+ } else {
+ Log.i("ddms", " (recycling client connection as well)");
+ // we should drop the client, but also attempt to reopen it.
+ // This is done by the DeviceMonitor.
+ client.getDevice().getMonitor().addClientToDropAndReopen(client,
+ IDebugPortProvider.NO_STATIC_PORT);
+ }
+ }
+ }
+
+ /*
+ * Tell the thread that something has changed.
+ */
+ private void wakeup() {
+ mSelector.wakeup();
+ }
+
+ /**
+ * Tell the thread to stop. Called from UI thread.
+ */
+ synchronized void quit() {
+ mQuit = true;
+ wakeup();
+ Log.d("ddms", "Waiting for Monitor thread");
+ try {
+ this.join();
+ // since we're quitting, lets drop all the client and disconnect
+ // the DebugSelectedPort
+ synchronized (mClientList) {
+ for (Client c : mClientList) {
+ c.close(false /* notify */);
+ broadcast(CLIENT_DISCONNECTED, c);
+ }
+ mClientList.clear();
+ }
+
+ if (mDebugSelectedChan != null) {
+ mDebugSelectedChan.close();
+ mDebugSelectedChan.socket().close();
+ mDebugSelectedChan = null;
+ }
+ mSelector.close();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ mInstance = null;
+ }
+
+ /**
+ * Add a new Client to the list of things we monitor. Also adds the client's
+ * channel and the client's debugger listener to the selection list. This
+ * should only be called from one thread (the VMWatcherThread) to avoid a
+ * race between "alreadyOpen" and Client creation.
+ */
+ synchronized void addClient(Client client) {
+ if (mInstance == null) {
+ return;
+ }
+
+ Log.d("ddms", "Adding new client " + client);
+
+ synchronized (mClientList) {
+ mClientList.add(client);
+
+ /*
+ * Register the Client's socket channel with the selector. We attach
+ * the Client to the SelectionKey. If you try to register a new
+ * channel with the Selector while it is waiting for I/O, you will
+ * block. The solution is to call wakeup() and then hold a lock to
+ * ensure that the registration happens before the Selector goes
+ * back to sleep.
+ */
+ try {
+ wakeup();
+
+ client.register(mSelector);
+
+ Debugger dbg = client.getDebugger();
+ if (dbg != null) {
+ dbg.registerListener(mSelector);
+ }
+ } catch (IOException ioe) {
+ // not really expecting this to happen
+ ioe.printStackTrace();
+ }
+ }
+ }
+
+ /*
+ * Broadcast an event to all message handlers.
+ */
+ private void broadcast(int event, Client client) {
+ Log.d("ddms", "broadcast " + event + ": " + client);
+
+ /*
+ * The handler objects appear once in mHandlerMap for each message they
+ * handle. We want to notify them once each, so we convert the HashMap
+ * to a HashSet before we iterate.
+ */
+ HashSet<ChunkHandler> set;
+ synchronized (mHandlerMap) {
+ Collection<ChunkHandler> values = mHandlerMap.values();
+ set = new HashSet<ChunkHandler>(values);
+ }
+
+ Iterator<ChunkHandler> iter = set.iterator();
+ while (iter.hasNext()) {
+ ChunkHandler handler = iter.next();
+ switch (event) {
+ case CLIENT_READY:
+ try {
+ handler.clientReady(client);
+ } catch (IOException ioe) {
+ // Something failed with the client. It should
+ // fall out of the list the next time we try to
+ // do something with it, so we discard the
+ // exception here and assume cleanup will happen
+ // later. May need to propagate farther. The
+ // trouble is that not all values for "event" may
+ // actually throw an exception.
+ Log.w("ddms",
+ "Got exception while broadcasting 'ready'");
+ return;
+ }
+ break;
+ case CLIENT_DISCONNECTED:
+ handler.clientDisconnected(client);
+ break;
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ }
+
+ /**
+ * Opens (or reopens) the "debug selected" port and listen for connections.
+ * @return true if the port was opened successfully.
+ * @throws IOException
+ */
+ private boolean reopenDebugSelectedPort() throws IOException {
+
+ Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort);
+ if (mDebugSelectedChan != null) {
+ mDebugSelectedChan.close();
+ }
+
+ mDebugSelectedChan = ServerSocketChannel.open();
+ mDebugSelectedChan.configureBlocking(false); // required for Selector
+
+ InetSocketAddress addr = new InetSocketAddress(
+ InetAddress.getByName("localhost"), //$NON-NLS-1$
+ mNewDebugSelectedPort);
+ mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR
+
+ try {
+ mDebugSelectedChan.socket().bind(addr);
+ if (mSelectedClient != null) {
+ mSelectedClient.update(Client.CHANGE_PORT);
+ }
+
+ mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this);
+
+ return true;
+ } catch (java.net.BindException e) {
+ displayDebugSelectedBindError(mNewDebugSelectedPort);
+
+ // do not attempt to reopen it.
+ mDebugSelectedChan = null;
+ mNewDebugSelectedPort = -1;
+
+ return false;
+ }
+ }
+
+ /*
+ * We have some activity on the "debug selected" port. Handle it.
+ */
+ private void processDebugSelectedActivity(SelectionKey key) {
+ assert key.isAcceptable();
+
+ ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel();
+
+ /*
+ * Find the debugger associated with the currently-selected client.
+ */
+ if (mSelectedClient != null) {
+ Debugger dbg = mSelectedClient.getDebugger();
+
+ if (dbg != null) {
+ Log.i("ddms", "Accepting connection on 'debug selected' port");
+ try {
+ acceptNewDebugger(dbg, acceptChan);
+ } catch (IOException ioe) {
+ // client should be gone, keep going
+ }
+
+ return;
+ }
+ }
+
+ Log.w("ddms",
+ "Connection on 'debug selected' port, but none selected");
+ try {
+ SocketChannel chan = acceptChan.accept();
+ chan.close();
+ } catch (IOException ioe) {
+ // not expected; client should be gone, keep going
+ } catch (NotYetBoundException e) {
+ displayDebugSelectedBindError(mDebugSelectedPort);
+ }
+ }
+
+ private void displayDebugSelectedBindError(int port) {
+ String message = String.format(
+ "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.",
+ port);
+
+ Log.logAndDisplay(LogLevel.ERROR, "ddms", message);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java
new file mode 100644
index 0000000..24dbb05
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+/**
+ * Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the
+ * socket, and convert it into {@link String} objects.
+ * <p/>Additionally, it splits the string by lines.
+ * <p/>Classes extending it must implement {@link #processNewLines(String[])} which receives
+ * new parsed lines as they become available.
+ */
+public abstract class MultiLineReceiver implements IShellOutputReceiver {
+
+ private boolean mTrimLines = true;
+
+ /** unfinished message line, stored for next packet */
+ private String mUnfinishedLine = null;
+
+ private final ArrayList<String> mArray = new ArrayList<String>();
+
+ /**
+ * Set the trim lines flag.
+ * @param trim hether the lines are trimmed, or not.
+ */
+ public void setTrimLine(boolean trim) {
+ mTrimLines = trim;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(
+ * byte[], int, int)
+ */
+ public final void addOutput(byte[] data, int offset, int length) {
+ if (isCancelled() == false) {
+ String s = null;
+ try {
+ s = new String(data, offset, length, "ISO-8859-1"); //$NON-NLS-1$
+ } catch (UnsupportedEncodingException e) {
+ // normal encoding didn't work, try the default one
+ s = new String(data, offset,length);
+ }
+
+ // ok we've got a string
+ if (s != null) {
+ // if we had an unfinished line we add it.
+ if (mUnfinishedLine != null) {
+ s = mUnfinishedLine + s;
+ mUnfinishedLine = null;
+ }
+
+ // now we split the lines
+ mArray.clear();
+ int start = 0;
+ do {
+ int index = s.indexOf("\r\n", start); //$NON-NLS-1$
+
+ // if \r\n was not found, this is an unfinished line
+ // and we store it to be processed for the next packet
+ if (index == -1) {
+ mUnfinishedLine = s.substring(start);
+ break;
+ }
+
+ // so we found a \r\n;
+ // extract the line
+ String line = s.substring(start, index);
+ if (mTrimLines) {
+ line = line.trim();
+ }
+ mArray.add(line);
+
+ // move start to after the \r\n we found
+ start = index + 2;
+ } while (true);
+
+ if (mArray.size() > 0) {
+ // at this point we've split all the lines.
+ // make the array
+ String[] lines = mArray.toArray(new String[mArray.size()]);
+
+ // send it for final processing
+ processNewLines(lines);
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
+ */
+ public final void flush() {
+ if (mUnfinishedLine != null) {
+ processNewLines(new String[] { mUnfinishedLine });
+ }
+
+ done();
+ }
+
+ /**
+ * Terminates the process. This is called after the last lines have been through
+ * {@link #processNewLines(String[])}.
+ */
+ public void done() {
+ // do nothing.
+ }
+
+ /**
+ * Called when new lines are being received by the remote process.
+ * <p/>It is guaranteed that the lines are complete when they are given to this method.
+ * @param lines The array containing the new lines.
+ */
+ public abstract void processNewLines(String[] lines);
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java
new file mode 100644
index 0000000..956b004
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Stores native allocation information.
+ * <p/>Contains number of allocations, their size and the stack trace.
+ * <p/>Note: the ddmlib does not resolve the stack trace automatically. While this class provides
+ * storage for resolved stack trace, this is merely for convenience.
+ */
+public final class NativeAllocationInfo {
+ /* constants for flag bits */
+ private static final int FLAG_ZYGOTE_CHILD = (1<<31);
+ private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD);
+
+ /**
+ * list of alloc functions that are filtered out when attempting to display
+ * a relevant method responsible for an allocation
+ */
+ private static ArrayList<String> sAllocFunctionFilter;
+ static {
+ sAllocFunctionFilter = new ArrayList<String>();
+ sAllocFunctionFilter.add("malloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("calloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("realloc"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("get_backtrace"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("get_hash"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("??"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("internal_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("operator new"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("leak_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("chk_free"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("chk_memalign"); //$NON-NLS-1$
+ sAllocFunctionFilter.add("Malloc"); //$NON-NLS-1$
+ }
+
+ private final int mSize;
+
+ private final boolean mIsZygoteChild;
+
+ private final int mAllocations;
+
+ private final ArrayList<Long> mStackCallAddresses = new ArrayList<Long>();
+
+ private ArrayList<NativeStackCallInfo> mResolvedStackCall = null;
+
+ private boolean mIsStackCallResolved = false;
+
+ /**
+ * Constructs a new {@link NativeAllocationInfo}.
+ * @param size The size of the allocations.
+ * @param allocations the allocation count
+ */
+ NativeAllocationInfo(int size, int allocations) {
+ this.mSize = size & ~FLAG_MASK;
+ this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0);
+ this.mAllocations = allocations;
+ }
+
+ /**
+ * Adds a stack call address for this allocation.
+ * @param address The address to add.
+ */
+ void addStackCallAddress(long address) {
+ mStackCallAddresses.add(address);
+ }
+
+ /**
+ * Returns the total size of this allocation.
+ */
+ public int getSize() {
+ return mSize;
+ }
+
+ /**
+ * Returns whether the allocation happened in a child of the zygote
+ * process.
+ */
+ public boolean isZygoteChild() {
+ return mIsZygoteChild;
+ }
+
+ /**
+ * Returns the allocation count.
+ */
+ public int getAllocationCount() {
+ return mAllocations;
+ }
+
+ /**
+ * Returns whether the stack call addresses have been resolved into
+ * {@link NativeStackCallInfo} objects.
+ */
+ public boolean isStackCallResolved() {
+ return mIsStackCallResolved;
+ }
+
+ /**
+ * Returns the stack call of this allocation as raw addresses.
+ * @return the list of addresses where the allocation happened.
+ */
+ public Long[] getStackCallAddresses() {
+ return mStackCallAddresses.toArray(new Long[mStackCallAddresses.size()]);
+ }
+
+ /**
+ * Sets the resolved stack call for this allocation.
+ * <p/>
+ * If <code>resolvedStackCall</code> is non <code>null</code> then
+ * {@link #isStackCallResolved()} will return <code>true</code> after this call.
+ * @param resolvedStackCall The list of {@link NativeStackCallInfo}.
+ */
+ public synchronized void setResolvedStackCall(List<NativeStackCallInfo> resolvedStackCall) {
+ if (mResolvedStackCall == null) {
+ mResolvedStackCall = new ArrayList<NativeStackCallInfo>();
+ } else {
+ mResolvedStackCall.clear();
+ }
+ mResolvedStackCall.addAll(resolvedStackCall);
+ mIsStackCallResolved = mResolvedStackCall.size() != 0;
+ }
+
+ /**
+ * Returns the resolved stack call.
+ * @return An array of {@link NativeStackCallInfo} or <code>null</code> if the stack call
+ * was not resolved.
+ * @see #setResolvedStackCall(ArrayList)
+ * @see #isStackCallResolved()
+ */
+ public synchronized NativeStackCallInfo[] getResolvedStackCall() {
+ if (mIsStackCallResolved) {
+ return mResolvedStackCall.toArray(new NativeStackCallInfo[mResolvedStackCall.size()]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ * @param obj the reference object with which to compare.
+ * @return <code>true</code> if this object is equal to the obj argument;
+ * <code>false</code> otherwise.
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this)
+ return true;
+ if (obj instanceof NativeAllocationInfo) {
+ NativeAllocationInfo mi = (NativeAllocationInfo)obj;
+ // quick compare of size, alloc, and stackcall size
+ if (mSize != mi.mSize || mAllocations != mi.mAllocations ||
+ mStackCallAddresses.size() != mi.mStackCallAddresses.size()) {
+ return false;
+ }
+ // compare the stack addresses
+ int count = mStackCallAddresses.size();
+ for (int i = 0 ; i < count ; i++) {
+ long a = mStackCallAddresses.get(i);
+ long b = mi.mStackCallAddresses.get(i);
+ if (a != b) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of the object.
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append("Allocations: ");
+ buffer.append(mAllocations);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ buffer.append("Size: ");
+ buffer.append(mSize);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ buffer.append("Total Size: ");
+ buffer.append(mSize * mAllocations);
+ buffer.append("\n"); //$NON-NLS-1$
+
+ Iterator<Long> addrIterator = mStackCallAddresses.iterator();
+ Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
+
+ while (sourceIterator.hasNext()) {
+ long addr = addrIterator.next();
+ NativeStackCallInfo source = sourceIterator.next();
+ if (addr == 0)
+ continue;
+
+ if (source.getLineNumber() != -1) {
+ buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr,
+ source.getLibraryName(), source.getMethodName(),
+ source.getSourceFile(), source.getLineNumber()));
+ } else {
+ buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr,
+ source.getLibraryName(), source.getMethodName(), source.getSourceFile()));
+ }
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the first {@link NativeStackCallInfo} that is relevant.
+ * <p/>
+ * A relevant <code>NativeStackCallInfo</code> is a stack call that is not deep in the
+ * lower level of the libc, but the actual method that performed the allocation.
+ * @return a <code>NativeStackCallInfo</code> or <code>null</code> if the stack call has not
+ * been processed from the raw addresses.
+ * @see #setResolvedStackCall(ArrayList)
+ * @see #isStackCallResolved()
+ */
+ public synchronized NativeStackCallInfo getRelevantStackCallInfo() {
+ if (mIsStackCallResolved && mResolvedStackCall != null) {
+ Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
+ Iterator<Long> addrIterator = mStackCallAddresses.iterator();
+
+ while (sourceIterator.hasNext() && addrIterator.hasNext()) {
+ long addr = addrIterator.next();
+ NativeStackCallInfo info = sourceIterator.next();
+ if (addr != 0 && info != null) {
+ if (isRelevant(info.getMethodName())) {
+ return info;
+ }
+ }
+ }
+
+ // couldnt find a relevant one, so we'll return the first one if it
+ // exists.
+ if (mResolvedStackCall.size() > 0)
+ return mResolvedStackCall.get(0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if the method name is relevant.
+ * @param methodName the method name to test.
+ */
+ private boolean isRelevant(String methodName) {
+ for (String filter : sAllocFunctionFilter) {
+ if (methodName.contains(filter)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java
new file mode 100644
index 0000000..5a26317
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Memory address to library mapping for native libraries.
+ * <p/>
+ * Each instance represents a single native library and its start and end memory addresses.
+ */
+public final class NativeLibraryMapInfo {
+ private long mStartAddr;
+ private long mEndAddr;
+
+ private String mLibrary;
+
+ /**
+ * Constructs a new native library map info.
+ * @param startAddr The start address of the library.
+ * @param endAddr The end address of the library.
+ * @param library The name of the library.
+ */
+ NativeLibraryMapInfo(long startAddr, long endAddr, String library) {
+ this.mStartAddr = startAddr;
+ this.mEndAddr = endAddr;
+ this.mLibrary = library;
+ }
+
+ /**
+ * Returns the name of the library.
+ */
+ public String getLibraryName() {
+ return mLibrary;
+ }
+
+ /**
+ * Returns the start address of the library.
+ */
+ public long getStartAddress() {
+ return mStartAddr;
+ }
+
+ /**
+ * Returns the end address of the library.
+ */
+ public long getEndAddress() {
+ return mEndAddr;
+ }
+
+ /**
+ * Returns whether the specified address is inside the library.
+ * @param address The address to test.
+ * @return <code>true</code> if the address is between the start and end address of the library.
+ * @see #getStartAddress()
+ * @see #getEndAddress()
+ */
+ public boolean isWithinLibrary(long address) {
+ return address >= mStartAddr && address <= mEndAddr;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java
new file mode 100644
index 0000000..e54818d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a stack call. This is used to return all of the call
+ * information as one object.
+ */
+public final class NativeStackCallInfo {
+ private final static Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$");
+
+ /** name of the library */
+ private String mLibrary;
+
+ /** name of the method */
+ private String mMethod;
+
+ /**
+ * name of the source file + line number in the format<br>
+ * &lt;sourcefile&gt;:&lt;linenumber&gt;
+ */
+ private String mSourceFile;
+
+ private int mLineNumber = -1;
+
+ /**
+ * Basic constructor with library, method, and sourcefile information
+ *
+ * @param lib The name of the library
+ * @param method the name of the method
+ * @param sourceFile the name of the source file and the line number
+ * as "[sourcefile]:[fileNumber]"
+ */
+ public NativeStackCallInfo(String lib, String method, String sourceFile) {
+ mLibrary = lib;
+ mMethod = method;
+
+ Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile);
+ if (m.matches()) {
+ mSourceFile = m.group(1);
+ try {
+ mLineNumber = Integer.parseInt(m.group(2));
+ } catch (NumberFormatException e) {
+ // do nothing, the line number will stay at -1
+ }
+ } else {
+ mSourceFile = sourceFile;
+ }
+ }
+
+ /**
+ * Returns the name of the library name.
+ */
+ public String getLibraryName() {
+ return mLibrary;
+ }
+
+ /**
+ * Returns the name of the method.
+ */
+ public String getMethodName() {
+ return mMethod;
+ }
+
+ /**
+ * Returns the name of the source file.
+ */
+ public String getSourceFile() {
+ return mSourceFile;
+ }
+
+ /**
+ * Returns the line number, or -1 if unknown.
+ */
+ public int getLineNumber() {
+ return mLineNumber;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java
new file mode 100644
index 0000000..d2b5a1e
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Implementation of {@link IShellOutputReceiver} that does nothing.
+ * <p/>This can be used to execute a remote shell command when the output is not needed.
+ */
+public final class NullOutputReceiver implements IShellOutputReceiver {
+
+ private static NullOutputReceiver sReceiver = new NullOutputReceiver();
+
+ public static IShellOutputReceiver getReceiver() {
+ return sReceiver;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int)
+ */
+ public void addOutput(byte[] data, int offset, int length) {
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
+ */
+ public void flush() {
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled()
+ */
+ public boolean isCancelled() {
+ return false;
+ }
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java
new file mode 100644
index 0000000..610cb59
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Data representing an image taken from a device frame buffer.
+ */
+public final class RawImage {
+ /**
+ * bit-per-pixel value.
+ */
+ public int bpp;
+ public int size;
+ public int width;
+ public int height;
+
+ public byte[] data;
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java
new file mode 100644
index 0000000..44df000
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java
@@ -0,0 +1,949 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+import com.android.ddmlib.AdbHelper.AdbResponse;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+
+/**
+ * Sync service class to push/pull to/from devices/emulators, through the debug bridge.
+ * <p/>
+ * To get a {@link SyncService} object, use {@link Device#getSyncService()}.
+ */
+public final class SyncService {
+
+ private final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' };
+ private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' };
+ private final static byte[] ID_STAT = { 'S', 'T', 'A', 'T' };
+ private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' };
+ private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' };
+ private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' };
+ private final static byte[] ID_SEND = { 'S', 'E', 'N', 'D' };
+// private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' };
+// private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' };
+
+ private final static NullSyncProgresMonitor sNullSyncProgressMonitor =
+ new NullSyncProgresMonitor();
+
+ private final static int S_ISOCK = 0xC000; // type: symbolic link
+ private final static int S_IFLNK = 0xA000; // type: symbolic link
+ private final static int S_IFREG = 0x8000; // type: regular file
+ private final static int S_IFBLK = 0x6000; // type: block device
+ private final static int S_IFDIR = 0x4000; // type: directory
+ private final static int S_IFCHR = 0x2000; // type: character device
+ private final static int S_IFIFO = 0x1000; // type: fifo
+/*
+ private final static int S_ISUID = 0x0800; // set-uid bit
+ private final static int S_ISGID = 0x0400; // set-gid bit
+ private final static int S_ISVTX = 0x0200; // sticky bit
+ private final static int S_IRWXU = 0x01C0; // user permissions
+ private final static int S_IRUSR = 0x0100; // user: read
+ private final static int S_IWUSR = 0x0080; // user: write
+ private final static int S_IXUSR = 0x0040; // user: execute
+ private final static int S_IRWXG = 0x0038; // group permissions
+ private final static int S_IRGRP = 0x0020; // group: read
+ private final static int S_IWGRP = 0x0010; // group: write
+ private final static int S_IXGRP = 0x0008; // group: execute
+ private final static int S_IRWXO = 0x0007; // other permissions
+ private final static int S_IROTH = 0x0004; // other: read
+ private final static int S_IWOTH = 0x0002; // other: write
+ private final static int S_IXOTH = 0x0001; // other: execute
+*/
+
+ private final static int SYNC_DATA_MAX = 64*1024;
+ private final static int REMOTE_PATH_MAX_LENGTH = 1024;
+
+ /** Result code for transfer success. */
+ public static final int RESULT_OK = 0;
+ /** Result code for canceled transfer */
+ public static final int RESULT_CANCELED = 1;
+ /** Result code for unknown error */
+ public static final int RESULT_UNKNOWN_ERROR = 2;
+ /** Result code for network connection error */
+ public static final int RESULT_CONNECTION_ERROR = 3;
+ /** Result code for unknown remote object during a pull */
+ public static final int RESULT_NO_REMOTE_OBJECT = 4;
+ /** Result code when attempting to pull multiple files into a file */
+ public static final int RESULT_TARGET_IS_FILE = 5;
+ /** Result code when attempting to pull multiple into a directory that does not exist. */
+ public static final int RESULT_NO_DIR_TARGET = 6;
+ /** Result code for wrong encoding on the remote path. */
+ public static final int RESULT_REMOTE_PATH_ENCODING = 7;
+ /** Result code for remote path that is too long. */
+ public static final int RESULT_REMOTE_PATH_LENGTH = 8;
+ /** Result code for error while writing local file. */
+ public static final int RESULT_FILE_WRITE_ERROR = 9;
+ /** Result code for error while reading local file. */
+ public static final int RESULT_FILE_READ_ERROR = 10;
+ /** Result code for attempting to push a file that does not exist. */
+ public static final int RESULT_NO_LOCAL_FILE = 11;
+ /** Result code for attempting to push a directory. */
+ public static final int RESULT_LOCAL_IS_DIRECTORY = 12;
+ /** Result code for when the target path of a multi file push is a file. */
+ public static final int RESULT_REMOTE_IS_FILE = 13;
+ /** Result code for receiving too much data from the remove device at once */
+ public static final int RESULT_BUFFER_OVERRUN = 14;
+
+ /**
+ * A file transfer result.
+ * <p/>
+ * This contains a code, and an optional string
+ */
+ public static class SyncResult {
+ private int mCode;
+ private String mMessage;
+ SyncResult(int code, String message) {
+ mCode = code;
+ mMessage = message;
+ }
+
+ SyncResult(int code, Exception e) {
+ this(code, e.getMessage());
+ }
+
+ SyncResult(int code) {
+ this(code, errorCodeToString(code));
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deal
+ * with displaying transfer progress.
+ */
+ public interface ISyncProgressMonitor {
+ /**
+ * Sent when the transfer starts
+ * @param totalWork the total amount of work.
+ */
+ public void start(int totalWork);
+ /**
+ * Sent when the transfer is finished or interrupted.
+ */
+ public void stop();
+ /**
+ * Sent to query for possible cancellation.
+ * @return true if the transfer should be stopped.
+ */
+ public boolean isCanceled();
+ /**
+ * Sent when a sub task is started.
+ * @param name the name of the sub task.
+ */
+ public void startSubTask(String name);
+ /**
+ * Sent when some progress have been made.
+ * @param work the amount of work done.
+ */
+ public void advance(int work);
+ }
+
+ /**
+ * A Sync progress monitor that does nothing
+ */
+ private static class NullSyncProgresMonitor implements ISyncProgressMonitor {
+ public void advance(int work) {
+ }
+ public boolean isCanceled() {
+ return false;
+ }
+
+ public void start(int totalWork) {
+ }
+ public void startSubTask(String name) {
+ }
+ public void stop() {
+ }
+ }
+
+ private InetSocketAddress mAddress;
+ private Device mDevice;
+ private SocketChannel mChannel;
+
+ /**
+ * Buffer used to send data. Allocated when needed and reused afterward.
+ */
+ private byte[] mBuffer;
+
+ /**
+ * Creates a Sync service object.
+ * @param address The address to connect to
+ * @param device the {@link Device} that the service connects to.
+ */
+ SyncService(InetSocketAddress address, Device device) {
+ mAddress = address;
+ mDevice = device;
+ }
+
+ /**
+ * Opens the sync connection. This must be called before any calls to push[File] / pull[File].
+ * @return true if the connection opened, false otherwise.
+ */
+ boolean openSync() {
+ try {
+ mChannel = SocketChannel.open(mAddress);
+ mChannel.configureBlocking(false);
+
+ // target a specific device
+ AdbHelper.setDevice(mChannel, mDevice);
+
+ byte[] request = AdbHelper.formAdbRequest("sync:"); // $NON-NLS-1$
+ AdbHelper.write(mChannel, request, -1, AdbHelper.STD_TIMEOUT);
+
+ AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */);
+
+ if (!resp.ioSuccess || !resp.okay) {
+ Log.w("ddms",
+ "Got timeout or unhappy response from ADB sync req: "
+ + resp.message);
+ mChannel.close();
+ mChannel = null;
+ return false;
+ }
+ } catch (IOException e) {
+ if (mChannel != null) {
+ try {
+ mChannel.close();
+ } catch (IOException e1) {
+ // we do nothing, since we'll return false just below
+ }
+ mChannel = null;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Closes the connection.
+ */
+ public void close() {
+ if (mChannel != null) {
+ try {
+ mChannel.close();
+ } catch (IOException e) {
+ // nothing to be done really...
+ }
+ mChannel = null;
+ }
+ }
+
+ /**
+ * Returns a sync progress monitor that does nothing. This allows background tasks that don't
+ * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}.
+ * <p/>This object can be reused multiple times and can be used by concurrent threads.
+ */
+ public static ISyncProgressMonitor getNullProgressMonitor() {
+ return sNullSyncProgressMonitor;
+ }
+
+ /**
+ * Converts an error code into a non-localized string
+ * @param code the error code;
+ */
+ private static String errorCodeToString(int code) {
+ switch (code) {
+ case RESULT_OK:
+ return "Success.";
+ case RESULT_CANCELED:
+ return "Tranfert canceled by the user.";
+ case RESULT_UNKNOWN_ERROR:
+ return "Unknown Error.";
+ case RESULT_CONNECTION_ERROR:
+ return "Adb Connection Error.";
+ case RESULT_NO_REMOTE_OBJECT:
+ return "Remote object doesn't exist!";
+ case RESULT_TARGET_IS_FILE:
+ return "Target object is a file.";
+ case RESULT_NO_DIR_TARGET:
+ return "Target directory doesn't exist.";
+ case RESULT_REMOTE_PATH_ENCODING:
+ return "Remote Path encoding is not supported.";
+ case RESULT_REMOTE_PATH_LENGTH:
+ return "Remove path is too long.";
+ case RESULT_FILE_WRITE_ERROR:
+ return "Writing local file failed!";
+ case RESULT_FILE_READ_ERROR:
+ return "Reading local file failed!";
+ case RESULT_NO_LOCAL_FILE:
+ return "Local file doesn't exist.";
+ case RESULT_LOCAL_IS_DIRECTORY:
+ return "Local path is a directory.";
+ case RESULT_REMOTE_IS_FILE:
+ return "Remote path is a file.";
+ case RESULT_BUFFER_OVERRUN:
+ return "Receiving too much data.";
+ }
+
+ throw new RuntimeException();
+ }
+
+ /**
+ * Pulls file(s) or folder(s).
+ * @param entries the remote item(s) to pull
+ * @param localPath The local destination. If the entries count is > 1 or
+ * if the unique entry is a folder, this should be a folder.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ *
+ * @see FileListingService.FileEntry
+ * @see #getNullProgressMonitor()
+ */
+ public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) {
+
+ // first we check the destination is a directory and exists
+ File f = new File(localPath);
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_DIR_TARGET);
+ }
+ if (f.isDirectory() == false) {
+ return new SyncResult(RESULT_TARGET_IS_FILE);
+ }
+
+ // get a FileListingService object
+ FileListingService fls = new FileListingService(mDevice);
+
+ // compute the number of file to move
+ int total = getTotalRemoteFileSize(entries, fls);
+
+ // start the monitor
+ monitor.start(total);
+
+ SyncResult result = doPull(entries, localPath, fls, monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * Pulls a single file.
+ * @param remote the remote file
+ * @param localFilename The local destination.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ *
+ * @see FileListingService.FileEntry
+ * @see #getNullProgressMonitor()
+ */
+ public SyncResult pullFile(FileEntry remote, String localFilename,
+ ISyncProgressMonitor monitor) {
+ int total = remote.getSizeValue();
+ monitor.start(total);
+
+ SyncResult result = doPullFile(remote.getFullPath(), localFilename, monitor);
+
+ monitor.stop();
+ return result;
+ }
+
+ /**
+ * Push several files.
+ * @param local An array of loca files to push
+ * @param remote the remote {@link FileEntry} representing a directory.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) {
+ if (remote.isDirectory() == false) {
+ return new SyncResult(RESULT_REMOTE_IS_FILE);
+ }
+
+ // make a list of File from the list of String
+ ArrayList<File> files = new ArrayList<File>();
+ for (String path : local) {
+ files.add(new File(path));
+ }
+
+ // get the total count of the bytes to transfer
+ File[] fileArray = files.toArray(new File[files.size()]);
+ int total = getTotalLocalFileSize(fileArray);
+
+ monitor.start(total);
+
+ SyncResult result = doPush(fileArray, remote.getFullPath(), monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * Push a single file.
+ * @param local the local filepath.
+ * @param remote The remote filepath.
+ * @param monitor The progress monitor. Cannot be null.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) {
+ File f = new File(local);
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_LOCAL_FILE);
+ }
+
+ if (f.isDirectory()) {
+ return new SyncResult(RESULT_LOCAL_IS_DIRECTORY);
+ }
+
+ monitor.start((int)f.length());
+
+ SyncResult result = doPushFile(local, remote, monitor);
+
+ monitor.stop();
+
+ return result;
+ }
+
+ /**
+ * compute the recursive file size of all the files in the list. Folder
+ * have a weight of 1.
+ * @param entries
+ * @param fls
+ * @return
+ */
+ private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) {
+ int count = 0;
+ for (FileEntry e : entries) {
+ int type = e.getType();
+ if (type == FileListingService.TYPE_DIRECTORY) {
+ // get the children
+ FileEntry[] children = fls.getChildren(e, false, null);
+ count += getTotalRemoteFileSize(children, fls) + 1;
+ } else if (type == FileListingService.TYPE_FILE) {
+ count += e.getSizeValue();
+ }
+ }
+
+ return count;
+ }
+
+ /**
+ * compute the recursive file size of all the files in the list. Folder
+ * have a weight of 1.
+ * This does not check for circular links.
+ * @param files
+ * @return
+ */
+ private int getTotalLocalFileSize(File[] files) {
+ int count = 0;
+
+ for (File f : files) {
+ if (f.exists()) {
+ if (f.isDirectory()) {
+ return getTotalLocalFileSize(f.listFiles()) + 1;
+ } else if (f.isFile()) {
+ count += f.length();
+ }
+ }
+ }
+
+ return count;
+ }
+
+ /**
+ * Pulls multiple files/folders recursively.
+ * @param entries The list of entry to pull
+ * @param localPath the localpath to a directory
+ * @param fileListingService a FileListingService object to browse through remote directories.
+ * @param monitor the progress monitor. Must be started already.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPull(FileEntry[] entries, String localPath,
+ FileListingService fileListingService,
+ ISyncProgressMonitor monitor) {
+
+ for (FileEntry e : entries) {
+ // check if we're cancelled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+
+ // get type (we only pull directory and files for now)
+ int type = e.getType();
+ if (type == FileListingService.TYPE_DIRECTORY) {
+ monitor.startSubTask(e.getFullPath());
+ String dest = localPath + File.separator + e.getName();
+
+ // make the directory
+ File d = new File(dest);
+ d.mkdir();
+
+ // then recursively call the content. Since we did a ls command
+ // to get the number of files, we can use the cache
+ FileEntry[] children = fileListingService.getChildren(e, true, null);
+ SyncResult result = doPull(children, dest, fileListingService, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ monitor.advance(1);
+ } else if (type == FileListingService.TYPE_FILE) {
+ monitor.startSubTask(e.getFullPath());
+ String dest = localPath + File.separator + e.getName();
+ SyncResult result = doPullFile(e.getFullPath(), dest, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ }
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+ /**
+ * Pulls a remote file
+ * @param remotePath the remote file (length max is 1024)
+ * @param localPath the local destination
+ * @param monitor the monitor. The monitor must be started already.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPullFile(String remotePath, String localPath,
+ ISyncProgressMonitor monitor) {
+ byte[] msg = null;
+ byte[] pullResult = new byte[8];
+ try {
+ byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+ if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+ return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
+ }
+
+ // create the full request message
+ msg = createFileReq(ID_RECV, remotePathContent);
+
+ // and send it.
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
+
+ // read the result, in a byte array containing 2 ints
+ // (id, size)
+ AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
+
+ // check we have the proper data back
+ if (checkResult(pullResult, ID_DATA) == false &&
+ checkResult(pullResult, ID_DONE) == false) {
+ return new SyncResult(RESULT_CONNECTION_ERROR);
+ }
+ } catch (UnsupportedEncodingException e) {
+ return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // access the destination file
+ File f = new File(localPath);
+
+ // create the stream to write in the file. We use a new try/catch block to differentiate
+ // between file and network io exceptions.
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(f);
+ } catch (FileNotFoundException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+
+ // the buffer to read the data
+ byte[] data = new byte[SYNC_DATA_MAX];
+
+ // loop to get data until we're done.
+ while (true) {
+ // check if we're cancelled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+
+ // if we're done, we stop the loop
+ if (checkResult(pullResult, ID_DONE)) {
+ break;
+ }
+ if (checkResult(pullResult, ID_DATA) == false) {
+ // hmm there's an error
+ return new SyncResult(RESULT_CONNECTION_ERROR);
+ }
+ int length = ArrayHelper.swap32bitFromArray(pullResult, 4);
+ if (length > SYNC_DATA_MAX) {
+ // buffer overrun!
+ // error and exit
+ return new SyncResult(RESULT_BUFFER_OVERRUN);
+ }
+
+ try {
+ // now read the length we received
+ AdbHelper.read(mChannel, data, length, AdbHelper.STD_TIMEOUT);
+
+ // get the header for the next packet.
+ AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // write the content in the file
+ try {
+ fos.write(data, 0, length);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+
+ monitor.advance(length);
+ }
+
+ try {
+ fos.flush();
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
+ }
+ return new SyncResult(RESULT_OK);
+ }
+
+
+ /**
+ * Push multiple files
+ * @param fileArray
+ * @param remotePath
+ * @param monitor
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) {
+ for (File f : fileArray) {
+ // check if we're canceled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+ if (f.exists()) {
+ if (f.isDirectory()) {
+ // append the name of the directory to the remote path
+ String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S
+ monitor.startSubTask(dest);
+ SyncResult result = doPush(f.listFiles(), dest, monitor);
+
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+
+ monitor.advance(1);
+ } else if (f.isFile()) {
+ // append the name of the file to the remote path
+ String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S
+ monitor.startSubTask(remoteFile);
+ SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor);
+ if (result.mCode != RESULT_OK) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+ /**
+ * Push a single file
+ * @param localPath the local file to push
+ * @param remotePath the remote file (length max is 1024)
+ * @param monitor the monitor. The monitor must be started already.
+ * @return a {@link SyncResult} object with a code and an optional message.
+ */
+ private SyncResult doPushFile(String localPath, String remotePath,
+ ISyncProgressMonitor monitor) {
+ FileInputStream fis = null;
+ byte[] msg;
+
+ try {
+ byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+ if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+ return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
+ }
+
+ File f = new File(localPath);
+
+ // this shouldn't happen but still...
+ if (f.exists() == false) {
+ return new SyncResult(RESULT_NO_LOCAL_FILE);
+ }
+
+ // create the stream to read the file
+ fis = new FileInputStream(f);
+
+ // create the header for the action
+ msg = createSendFileReq(ID_SEND, remotePathContent, 0644);
+ } catch (UnsupportedEncodingException e) {
+ return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
+ } catch (FileNotFoundException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ // and send it. We use a custom try/catch block to make the difference between
+ // file and network IO exceptions.
+ try {
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // create the buffer used to read.
+ // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning.
+ if (mBuffer == null) {
+ mBuffer = new byte[SYNC_DATA_MAX + 8];
+ }
+ System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length);
+
+ // look while there is something to read
+ while (true) {
+ // check if we're canceled
+ if (monitor.isCanceled() == true) {
+ return new SyncResult(RESULT_CANCELED);
+ }
+
+ // read up to SYNC_DATA_MAX
+ int readCount = 0;
+ try {
+ readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ if (readCount == -1) {
+ // we reached the end of the file
+ break;
+ }
+
+ // now send the data to the device
+ // first write the amount read
+ ArrayHelper.swap32bitsToArray(readCount, mBuffer, 4);
+
+ // now write it
+ try {
+ AdbHelper.write(mChannel, mBuffer, readCount+8, AdbHelper.STD_TIMEOUT);
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ // and advance the monitor
+ monitor.advance(readCount);
+ }
+ // close the local file
+ try {
+ fis.close();
+ } catch (IOException e) {
+ return new SyncResult(RESULT_FILE_READ_ERROR, e);
+ }
+
+ try {
+ // create the DONE message
+ long time = System.currentTimeMillis() / 1000;
+ msg = createReq(ID_DONE, (int)time);
+
+ // and send it.
+ AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
+
+ // read the result, in a byte array containing 2 ints
+ // (id, size)
+ byte[] result = new byte[8];
+ AdbHelper.read(mChannel, result, -1 /* full length */, AdbHelper.STD_TIMEOUT);
+
+ if (checkResult(result, ID_OKAY) == false) {
+ if (checkResult(result, ID_FAIL)) {
+ // read some error message...
+ int len = ArrayHelper.swap32bitFromArray(result, 4);
+
+ AdbHelper.read(mChannel, mBuffer, len, AdbHelper.STD_TIMEOUT);
+
+ // output the result?
+ String message = new String(mBuffer, 0, len);
+ Log.e("ddms", "transfer error: " + message);
+ return new SyncResult(RESULT_UNKNOWN_ERROR, message);
+ }
+
+ return new SyncResult(RESULT_UNKNOWN_ERROR);
+ }
+ } catch (IOException e) {
+ return new SyncResult(RESULT_CONNECTION_ERROR, e);
+ }
+
+ return new SyncResult(RESULT_OK);
+ }
+
+
+ /**
+ * Returns the mode of the remote file.
+ * @param path the remote file
+ * @return and Integer containing the mode if all went well or null
+ * otherwise
+ */
+ private Integer readMode(String path) {
+ try {
+ // create the stat request message.
+ byte[] msg = createFileReq(ID_STAT, path);
+
+ AdbHelper.write(mChannel, msg, -1 /* full length */, AdbHelper.STD_TIMEOUT);
+
+ // read the result, in a byte array containing 4 ints
+ // (id, mode, size, time)
+ byte[] statResult = new byte[16];
+ AdbHelper.read(mChannel, statResult, -1 /* full length */, AdbHelper.STD_TIMEOUT);
+
+ // check we have the proper data back
+ if (checkResult(statResult, ID_STAT) == false) {
+ return null;
+ }
+
+ // we return the mode (2nd int in the array)
+ return ArrayHelper.swap32bitFromArray(statResult, 4);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Create a command with a code and an int values
+ * @param command
+ * @param value
+ * @return
+ */
+ private static byte[] createReq(byte[] command, int value) {
+ byte[] array = new byte[8];
+
+ System.arraycopy(command, 0, array, 0, 4);
+ ArrayHelper.swap32bitsToArray(value, array, 4);
+
+ return array;
+ }
+
+ /**
+ * Creates the data array for a stat request.
+ * @param command the 4 byte command (ID_STAT, ID_RECV, ...)
+ * @param path The path of the remote file on which to execute the command
+ * @return the byte[] to send to the device through adb
+ */
+ private static byte[] createFileReq(byte[] command, String path) {
+ byte[] pathContent = null;
+ try {
+ pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+
+ return createFileReq(command, pathContent);
+ }
+
+ /**
+ * Creates the data array for a file request. This creates an array with a 4 byte command + the
+ * remote file name.
+ * @param command the 4 byte command (ID_STAT, ID_RECV, ...).
+ * @param path The path, as a byte array, of the remote file on which to
+ * execute the command.
+ * @return the byte[] to send to the device through adb
+ */
+ private static byte[] createFileReq(byte[] command, byte[] path) {
+ byte[] array = new byte[8 + path.length];
+
+ System.arraycopy(command, 0, array, 0, 4);
+ ArrayHelper.swap32bitsToArray(path.length, array, 4);
+ System.arraycopy(path, 0, array, 8, path.length);
+
+ return array;
+ }
+
+ private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) {
+ // make the mode into a string
+ String modeStr = "," + (mode & 0777); // $NON-NLS-1S
+ byte[] modeContent = null;
+ try {
+ modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+
+ byte[] array = new byte[8 + path.length + modeContent.length];
+
+ System.arraycopy(command, 0, array, 0, 4);
+ ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4);
+ System.arraycopy(path, 0, array, 8, path.length);
+ System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length);
+
+ return array;
+
+
+ }
+
+ /**
+ * Checks the result array starts with the provided code
+ * @param result The result array to check
+ * @param code The 4 byte code.
+ * @return true if the code matches.
+ */
+ private static boolean checkResult(byte[] result, byte[] code) {
+ if (result[0] != code[0] ||
+ result[1] != code[1] ||
+ result[2] != code[2] ||
+ result[3] != code[3]) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+ private static int getFileType(int mode) {
+ if ((mode & S_ISOCK) == S_ISOCK) {
+ return FileListingService.TYPE_SOCKET;
+ }
+
+ if ((mode & S_IFLNK) == S_IFLNK) {
+ return FileListingService.TYPE_LINK;
+ }
+
+ if ((mode & S_IFREG) == S_IFREG) {
+ return FileListingService.TYPE_FILE;
+ }
+
+ if ((mode & S_IFBLK) == S_IFBLK) {
+ return FileListingService.TYPE_BLOCK;
+ }
+
+ if ((mode & S_IFDIR) == S_IFDIR) {
+ return FileListingService.TYPE_DIRECTORY;
+ }
+
+ if ((mode & S_IFCHR) == S_IFCHR) {
+ return FileListingService.TYPE_CHARACTER;
+ }
+
+ if ((mode & S_IFIFO) == S_IFIFO) {
+ return FileListingService.TYPE_FIFO;
+ }
+
+ return FileListingService.TYPE_OTHER;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java
new file mode 100644
index 0000000..8f284f3
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib;
+
+/**
+ * Holds a thread information.
+ */
+public final class ThreadInfo implements IStackTraceInfo {
+ private int mThreadId;
+ private String mThreadName;
+ private int mStatus;
+ private int mTid;
+ private int mUtime;
+ private int mStime;
+ private boolean mIsDaemon;
+ private StackTraceElement[] mTrace;
+ private long mTraceTime;
+
+ // priority?
+ // total CPU used?
+ // method at top of stack?
+
+ /**
+ * Construct with basic identification.
+ */
+ ThreadInfo(int threadId, String threadName) {
+ mThreadId = threadId;
+ mThreadName = threadName;
+
+ mStatus = -1;
+ //mTid = mUtime = mStime = 0;
+ //mIsDaemon = false;
+ }
+
+ /**
+ * Set with the values we get from a THST chunk.
+ */
+ void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) {
+
+ mStatus = status;
+ mTid = tid;
+ mUtime = utime;
+ mStime = stime;
+ mIsDaemon = isDaemon;
+ }
+
+ /**
+ * Sets the stack call of the thread.
+ * @param trace stackcall information.
+ */
+ void setStackCall(StackTraceElement[] trace) {
+ mTrace = trace;
+ mTraceTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Returns the thread's ID.
+ */
+ public int getThreadId() {
+ return mThreadId;
+ }
+
+ /**
+ * Returns the thread's name.
+ */
+ public String getThreadName() {
+ return mThreadName;
+ }
+
+ void setThreadName(String name) {
+ mThreadName = name;
+ }
+
+ /**
+ * Returns the system tid.
+ */
+ public int getTid() {
+ return mTid;
+ }
+
+ /**
+ * Returns the VM thread status.
+ */
+ public int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Returns the cumulative user time.
+ */
+ public int getUtime() {
+ return mUtime;
+ }
+
+ /**
+ * Returns the cumulative system time.
+ */
+ public int getStime() {
+ return mStime;
+ }
+
+ /**
+ * Returns whether this is a daemon thread.
+ */
+ public boolean isDaemon() {
+ return mIsDaemon;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
+ */
+ public StackTraceElement[] getStackTrace() {
+ return mTrace;
+ }
+
+ /**
+ * Returns the approximate time of the stacktrace data.
+ * @see #getStackTrace()
+ */
+ public long getStackCallTime() {
+ return mTraceTime;
+ }
+}
+
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java
new file mode 100644
index 0000000..ec9186c
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents an event and its data.
+ */
+public class EventContainer {
+
+ /**
+ * Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)}
+ *
+ */
+ public enum CompareMethod {
+ EQUAL_TO("equals", "=="),
+ LESSER_THAN("less than or equals to", "<="),
+ LESSER_THAN_STRICT("less than", "<"),
+ GREATER_THAN("greater than or equals to", ">="),
+ GREATER_THAN_STRICT("greater than", ">"),
+ BIT_CHECK("bit check", "&");
+
+ private final String mName;
+ private final String mTestString;
+
+ private CompareMethod(String name, String testString) {
+ mName = name;
+ mTestString = testString;
+ }
+
+ /**
+ * Returns the display string.
+ */
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+ /**
+ * Returns a short string representing the comparison.
+ */
+ public String testString() {
+ return mTestString;
+ }
+ }
+
+
+ /**
+ * Type for event data.
+ */
+ public static enum EventValueType {
+ UNKNOWN(0),
+ INT(1),
+ LONG(2),
+ STRING(3),
+ LIST(4),
+ TREE(5);
+
+ private final static Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$
+
+ private int mValue;
+
+ /**
+ * Returns a {@link EventValueType} from an integer value, or <code>null</code> if no match
+ * was found.
+ * @param value the integer value.
+ */
+ static EventValueType getEventValueType(int value) {
+ for (EventValueType type : values()) {
+ if (type.mValue == value) {
+ return type;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a storage string for an {@link Object} of type supported by
+ * {@link EventValueType}.
+ * <p/>
+ * Strings created by this method can be reloaded with
+ * {@link #getObjectFromStorageString(String)}.
+ * <p/>
+ * NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported.
+ * @param object the object to "convert" into a storage string.
+ * @return a string storing the object and its type or null if the type was not recognized.
+ */
+ public static String getStorageString(Object object) {
+ if (object instanceof String) {
+ return STRING.mValue + "@" + (String)object; //$NON-NLS-1$
+ } else if (object instanceof Integer) {
+ return INT.mValue + "@" + object.toString(); //$NON-NLS-1$
+ } else if (object instanceof Long) {
+ return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an {@link Object} from a storage string created with
+ * {@link #getStorageString(Object)}.
+ * @param value the storage string
+ * @return an {@link Object} or null if the string or type were not recognized.
+ */
+ public static Object getObjectFromStorageString(String value) {
+ Matcher m = STORAGE_PATTERN.matcher(value);
+ if (m.matches()) {
+ try {
+ EventValueType type = getEventValueType(Integer.parseInt(m.group(1)));
+
+ if (type == null) {
+ return null;
+ }
+
+ switch (type) {
+ case STRING:
+ return m.group(2);
+ case INT:
+ return Integer.valueOf(m.group(2));
+ case LONG:
+ return Long.valueOf(m.group(2));
+ }
+ } catch (NumberFormatException nfe) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the integer value of the enum.
+ */
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ private EventValueType(int value) {
+ mValue = value;
+ }
+ }
+
+ public int mTag;
+ public int pid; /* generating process's pid */
+ public int tid; /* generating process's tid */
+ public int sec; /* seconds since Epoch */
+ public int nsec; /* nanoseconds */
+
+ private Object mData;
+
+ /**
+ * Creates an {@link EventContainer} from a {@link LogEntry}.
+ * @param entry the LogEntry from which pid, tid, and time info is copied.
+ * @param tag the event tag value
+ * @param data the data of the EventContainer.
+ */
+ EventContainer(LogEntry entry, int tag, Object data) {
+ getType(data);
+ mTag = tag;
+ mData = data;
+
+ pid = entry.pid;
+ tid = entry.tid;
+ sec = entry.sec;
+ nsec = entry.nsec;
+ }
+
+ /**
+ * Creates an {@link EventContainer} with raw data
+ */
+ EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
+ getType(data);
+ mTag = tag;
+ mData = data;
+
+ this.pid = pid;
+ this.tid = tid;
+ this.sec = sec;
+ this.nsec = nsec;
+ }
+
+ /**
+ * Returns the data as an int.
+ * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}.
+ * @see #getType()
+ */
+ public final Integer getInt() throws InvalidTypeException {
+ if (getType(mData) == EventValueType.INT) {
+ return (Integer)mData;
+ }
+
+ throw new InvalidTypeException();
+ }
+
+ /**
+ * Returns the data as a long.
+ * @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}.
+ * @see #getType()
+ */
+ public final Long getLong() throws InvalidTypeException {
+ if (getType(mData) == EventValueType.LONG) {
+ return (Long)mData;
+ }
+
+ throw new InvalidTypeException();
+ }
+
+ /**
+ * Returns the data as a String.
+ * @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}.
+ * @see #getType()
+ */
+ public final String getString() throws InvalidTypeException {
+ if (getType(mData) == EventValueType.STRING) {
+ return (String)mData;
+ }
+
+ throw new InvalidTypeException();
+ }
+
+ /**
+ * Returns a value by index. The return type is defined by its type.
+ * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+ */
+ public Object getValue(int valueIndex) {
+ return getValue(mData, valueIndex, true);
+ }
+
+ /**
+ * Returns a value by index as a double.
+ * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+ * @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
+ * {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the
+ * list at index <code>valueIndex</code> is not of type {@link EventValueType#INT} or
+ * {@link EventValueType#LONG}.
+ * @see #getType()
+ */
+ public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
+ return getValueAsDouble(mData, valueIndex, true);
+ }
+
+ /**
+ * Returns a value by index as a String.
+ * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+ * @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
+ * {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST},
+ * or if the item in the list at index <code>valueIndex</code> is not of type
+ * {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING}
+ * @see #getType()
+ */
+ public String getValueAsString(int valueIndex) throws InvalidTypeException {
+ return getValueAsString(mData, valueIndex, true);
+ }
+
+ /**
+ * Returns the type of the data.
+ */
+ public EventValueType getType() {
+ return getType(mData);
+ }
+
+ /**
+ * Returns the type of an object.
+ */
+ public final EventValueType getType(Object data) {
+ if (data instanceof Integer) {
+ return EventValueType.INT;
+ } else if (data instanceof Long) {
+ return EventValueType.LONG;
+ } else if (data instanceof String) {
+ return EventValueType.STRING;
+ } else if (data instanceof Object[]) {
+ // loop through the list to see if we have another list
+ Object[] objects = (Object[])data;
+ for (Object obj : objects) {
+ EventValueType type = getType(obj);
+ if (type == EventValueType.LIST || type == EventValueType.TREE) {
+ return EventValueType.TREE;
+ }
+ }
+ return EventValueType.LIST;
+ }
+
+ return EventValueType.UNKNOWN;
+ }
+
+ /**
+ * Checks that the <code>index</code>-th value of this event against a provided value.
+ * @param index the index of the value to test
+ * @param value the value to test against
+ * @param compareMethod the method of testing
+ * @return true if the test passed.
+ * @throws InvalidTypeException in case of type mismatch between the value to test and the value
+ * to test against, or if the compare method is incompatible with the type of the values.
+ * @see CompareMethod
+ */
+ public boolean testValue(int index, Object value,
+ CompareMethod compareMethod) throws InvalidTypeException {
+ EventValueType type = getType(mData);
+ if (index > 0 && type != EventValueType.LIST) {
+ throw new InvalidTypeException();
+ }
+
+ Object data = mData;
+ if (type == EventValueType.LIST) {
+ data = ((Object[])mData)[index];
+ }
+
+ if (data.getClass().equals(data.getClass()) == false) {
+ throw new InvalidTypeException();
+ }
+
+ switch (compareMethod) {
+ case EQUAL_TO:
+ return data.equals(value);
+ case LESSER_THAN:
+ if (data instanceof Integer) {
+ return (((Integer)data).compareTo((Integer)value) <= 0);
+ } else if (data instanceof Long) {
+ return (((Long)data).compareTo((Long)value) <= 0);
+ }
+
+ // other types can't use this compare method.
+ throw new InvalidTypeException();
+ case LESSER_THAN_STRICT:
+ if (data instanceof Integer) {
+ return (((Integer)data).compareTo((Integer)value) < 0);
+ } else if (data instanceof Long) {
+ return (((Long)data).compareTo((Long)value) < 0);
+ }
+
+ // other types can't use this compare method.
+ throw new InvalidTypeException();
+ case GREATER_THAN:
+ if (data instanceof Integer) {
+ return (((Integer)data).compareTo((Integer)value) >= 0);
+ } else if (data instanceof Long) {
+ return (((Long)data).compareTo((Long)value) >= 0);
+ }
+
+ // other types can't use this compare method.
+ throw new InvalidTypeException();
+ case GREATER_THAN_STRICT:
+ if (data instanceof Integer) {
+ return (((Integer)data).compareTo((Integer)value) > 0);
+ } else if (data instanceof Long) {
+ return (((Long)data).compareTo((Long)value) > 0);
+ }
+
+ // other types can't use this compare method.
+ throw new InvalidTypeException();
+ case BIT_CHECK:
+ if (data instanceof Integer) {
+ return (((Integer)data).intValue() & ((Integer)value).intValue()) != 0;
+ } else if (data instanceof Long) {
+ return (((Long)data).longValue() & ((Long)value).longValue()) != 0;
+ }
+
+ // other types can't use this compare method.
+ throw new InvalidTypeException();
+ default :
+ throw new InvalidTypeException();
+ }
+ }
+
+ private final Object getValue(Object data, int valueIndex, boolean recursive) {
+ EventValueType type = getType(data);
+
+ switch (type) {
+ case INT:
+ case LONG:
+ case STRING:
+ return data;
+ case LIST:
+ if (recursive) {
+ Object[] list = (Object[]) data;
+ if (valueIndex >= 0 && valueIndex < list.length) {
+ return getValue(list[valueIndex], valueIndex, false);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private final double getValueAsDouble(Object data, int valueIndex, boolean recursive)
+ throws InvalidTypeException {
+ EventValueType type = getType(data);
+
+ switch (type) {
+ case INT:
+ return ((Integer)data).doubleValue();
+ case LONG:
+ return ((Long)data).doubleValue();
+ case STRING:
+ throw new InvalidTypeException();
+ case LIST:
+ if (recursive) {
+ Object[] list = (Object[]) data;
+ if (valueIndex >= 0 && valueIndex < list.length) {
+ return getValueAsDouble(list[valueIndex], valueIndex, false);
+ }
+ }
+ }
+
+ throw new InvalidTypeException();
+ }
+
+ private final String getValueAsString(Object data, int valueIndex, boolean recursive)
+ throws InvalidTypeException {
+ EventValueType type = getType(data);
+
+ switch (type) {
+ case INT:
+ return ((Integer)data).toString();
+ case LONG:
+ return ((Long)data).toString();
+ case STRING:
+ return (String)data;
+ case LIST:
+ if (recursive) {
+ Object[] list = (Object[]) data;
+ if (valueIndex >= 0 && valueIndex < list.length) {
+ return getValueAsString(list[valueIndex], valueIndex, false);
+ }
+ } else {
+ throw new InvalidTypeException(
+ "getValueAsString() doesn't support EventValueType.TREE");
+ }
+ }
+
+ throw new InvalidTypeException(
+ "getValueAsString() unsupported type:" + type);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java
new file mode 100644
index 0000000..85e99c1
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for the "event" log.
+ */
+public final class EventLogParser {
+
+ /** Location of the tag map file on the device */
+ private final static String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$
+
+ /**
+ * Event log entry types. These must match up with the declarations in
+ * java/android/android/util/EventLog.java.
+ */
+ private final static int EVENT_TYPE_INT = 0;
+ private final static int EVENT_TYPE_LONG = 1;
+ private final static int EVENT_TYPE_STRING = 2;
+ private final static int EVENT_TYPE_LIST = 3;
+
+ private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
+ "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
+ private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
+ "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
+ private final static Pattern PATTERN_DESCRIPTION = Pattern.compile(
+ "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$
+
+ private final static Pattern TEXT_LOG_LINE = Pattern.compile(
+ "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$
+
+ private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>();
+
+ private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap =
+ new TreeMap<Integer, EventValueDescription[]>();
+
+ public EventLogParser() {
+ }
+
+ /**
+ * Inits the parser for a specific Device.
+ * <p/>
+ * This methods reads the event-log-tags located on the device to find out
+ * what tags are being written to the event log and what their format is.
+ * @param device The device.
+ * @return <code>true</code> if success, <code>false</code> if failure or cancellation.
+ */
+ public boolean init(Device device) {
+ // read the event tag map file on the device.
+ try {
+ device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$
+ new MultiLineReceiver() {
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ processTagLine(line);
+ }
+ }
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Inits the parser with the content of a tag file.
+ * @param tagFileContent the lines of a tag file.
+ * @return <code>true</code> if success, <code>false</code> if failure.
+ */
+ public boolean init(String[] tagFileContent) {
+ for (String line : tagFileContent) {
+ processTagLine(line);
+ }
+ return true;
+ }
+
+ /**
+ * Inits the parser with a specified event-log-tags file.
+ * @param filePath
+ * @return <code>true</code> if success, <code>false</code> if failure.
+ */
+ public boolean init(String filePath) {
+ try {
+ BufferedReader reader = new BufferedReader(new FileReader(filePath));
+
+ String line = null;
+ do {
+ line = reader.readLine();
+ if (line != null) {
+ processTagLine(line);
+ }
+ } while (line != null);
+
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Processes a line from the event-log-tags file.
+ * @param line the line to process
+ */
+ private void processTagLine(String line) {
+ // ignore empty lines and comment lines
+ if (line.length() > 0 && line.charAt(0) != '#') {
+ Matcher m = PATTERN_TAG_WITH_DESC.matcher(line);
+ if (m.matches()) {
+ try {
+ int value = Integer.parseInt(m.group(1));
+ String name = m.group(2);
+ if (name != null && mTagMap.get(value) == null) {
+ mTagMap.put(value, name);
+ }
+
+ // special case for the GC tag. We ignore what is in the file,
+ // and take what the custom GcEventContainer class tells us.
+ // This is due to the event encoding several values on 2 longs.
+ // @see GcEventContainer
+ if (value == GcEventContainer.GC_EVENT_TAG) {
+ mValueDescriptionMap.put(value,
+ GcEventContainer.getValueDescriptions());
+ } else {
+
+ String description = m.group(3);
+ if (description != null && description.length() > 0) {
+ EventValueDescription[] desc =
+ processDescription(description);
+
+ if (desc != null) {
+ mValueDescriptionMap.put(value, desc);
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ // failed to convert the number into a string. just ignore it.
+ }
+ } else {
+ m = PATTERN_SIMPLE_TAG.matcher(line);
+ if (m.matches()) {
+ int value = Integer.parseInt(m.group(1));
+ String name = m.group(2);
+ if (name != null && mTagMap.get(value) == null) {
+ mTagMap.put(value, name);
+ }
+ }
+ }
+ }
+ }
+
+ private EventValueDescription[] processDescription(String description) {
+ String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$
+
+ ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>();
+
+ for (String desc : descriptions) {
+ Matcher m = PATTERN_DESCRIPTION.matcher(desc);
+ if (m.matches()) {
+ try {
+ String name = m.group(1);
+
+ String typeString = m.group(2);
+ int typeValue = Integer.parseInt(typeString);
+ EventValueType eventValueType = EventValueType.getEventValueType(typeValue);
+ if (eventValueType == null) {
+ // just ignore this description if the value is not recognized.
+ // TODO: log the error.
+ }
+
+ typeString = m.group(3);
+ if (typeString != null && typeString.length() > 0) {
+ //skip the |
+ typeString = typeString.substring(1);
+
+ typeValue = Integer.parseInt(typeString);
+ ValueType valueType = ValueType.getValueType(typeValue);
+
+ list.add(new EventValueDescription(name, eventValueType, valueType));
+ } else {
+ list.add(new EventValueDescription(name, eventValueType));
+ }
+ } catch (NumberFormatException nfe) {
+ // just ignore this description if one number is malformed.
+ // TODO: log the error.
+ } catch (InvalidValueTypeException e) {
+ // just ignore this description if data type and data unit don't match
+ // TODO: log the error.
+ }
+ } else {
+ Log.e("EventLogParser", //$NON-NLS-1$
+ String.format("Can't parse %1$s", description)); //$NON-NLS-1$
+ }
+ }
+
+ if (list.size() == 0) {
+ return null;
+ }
+
+ return list.toArray(new EventValueDescription[list.size()]);
+
+ }
+
+ public EventContainer parse(LogEntry entry) {
+ if (entry.len < 4) {
+ return null;
+ }
+
+ int inOffset = 0;
+
+ int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset);
+ inOffset += 4;
+
+ String tag = mTagMap.get(tagValue);
+ if (tag == null) {
+ Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue));
+ }
+
+ ArrayList<Object> list = new ArrayList<Object>();
+ if (parseBinaryEvent(entry.data, inOffset, list) == -1) {
+ return null;
+ }
+
+ Object data;
+ if (list.size() == 1) {
+ data = list.get(0);
+ } else{
+ data = list.toArray();
+ }
+
+ EventContainer event = null;
+ if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+ event = new GcEventContainer(entry, tagValue, data);
+ } else {
+ event = new EventContainer(entry, tagValue, data);
+ }
+
+ return event;
+ }
+
+ public EventContainer parse(String textLogLine) {
+ // line will look like
+ // 04-29 23:16:16.691 I/dvm_gc_info( 427): <data>
+ // where <data> is either
+ // [value1,value2...]
+ // or
+ // value
+ if (textLogLine.length() == 0) {
+ return null;
+ }
+
+ // parse the header first
+ Matcher m = TEXT_LOG_LINE.matcher(textLogLine);
+ if (m.matches()) {
+ try {
+ int month = Integer.parseInt(m.group(1));
+ int day = Integer.parseInt(m.group(2));
+ int hours = Integer.parseInt(m.group(3));
+ int minutes = Integer.parseInt(m.group(4));
+ int seconds = Integer.parseInt(m.group(5));
+ int milliseconds = Integer.parseInt(m.group(6));
+
+ // convert into seconds since epoch and nano-seconds.
+ Calendar cal = Calendar.getInstance();
+ cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds);
+ int sec = (int)Math.floor(cal.getTimeInMillis()/1000);
+ int nsec = milliseconds * 1000000;
+
+ String tag = m.group(7);
+
+ // get the numerical tag value
+ int tagValue = -1;
+ Set<Entry<Integer, String>> tagSet = mTagMap.entrySet();
+ for (Entry<Integer, String> entry : tagSet) {
+ if (tag.equals(entry.getValue())) {
+ tagValue = entry.getKey();
+ break;
+ }
+ }
+
+ if (tagValue == -1) {
+ return null;
+ }
+
+ int pid = Integer.parseInt(m.group(8));
+
+ Object data = parseTextData(m.group(9), tagValue);
+ if (data == null) {
+ return null;
+ }
+
+ // now we can allocate and return the EventContainer
+ EventContainer event = null;
+ if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+ event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
+ } else {
+ event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
+ }
+
+ return event;
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ public Map<Integer, String> getTagMap() {
+ return mTagMap;
+ }
+
+ public Map<Integer, EventValueDescription[]> getEventInfoMap() {
+ return mValueDescriptionMap;
+ }
+
+ /**
+ * Recursively convert binary log data to printable form.
+ *
+ * This needs to be recursive because you can have lists of lists.
+ *
+ * If we run out of room, we stop processing immediately. It's important
+ * for us to check for space on every output element to avoid producing
+ * garbled output.
+ *
+ * Returns the amount read on success, -1 on failure.
+ */
+ private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) {
+
+ if (eventData.length - dataOffset < 1)
+ return -1;
+
+ int offset = dataOffset;
+
+ int type = eventData[offset++];
+
+ //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);
+
+ switch (type) {
+ case EVENT_TYPE_INT: { /* 32-bit signed int */
+ int ival;
+
+ if (eventData.length - offset < 4)
+ return -1;
+ ival = ArrayHelper.swap32bitFromArray(eventData, offset);
+ offset += 4;
+
+ list.add(new Integer(ival));
+ }
+ break;
+ case EVENT_TYPE_LONG: { /* 64-bit signed long */
+ long lval;
+
+ if (eventData.length - offset < 8)
+ return -1;
+ lval = ArrayHelper.swap64bitFromArray(eventData, offset);
+ offset += 8;
+
+ list.add(new Long(lval));
+ }
+ break;
+ case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */
+ int strLen;
+
+ if (eventData.length - offset < 4)
+ return -1;
+ strLen = ArrayHelper.swap32bitFromArray(eventData, offset);
+ offset += 4;
+
+ if (eventData.length - offset < strLen)
+ return -1;
+
+ // get the string
+ try {
+ String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$
+ list.add(str);
+ } catch (UnsupportedEncodingException e) {
+ }
+ offset += strLen;
+ break;
+ }
+ case EVENT_TYPE_LIST: { /* N items, all different types */
+
+ if (eventData.length - offset < 1)
+ return -1;
+
+ int count = eventData[offset++];
+
+ // make a new temp list
+ ArrayList<Object> subList = new ArrayList<Object>();
+ for (int i = 0; i < count; i++) {
+ int result = parseBinaryEvent(eventData, offset, subList);
+ if (result == -1) {
+ return result;
+ }
+
+ offset += result;
+ }
+
+ list.add(subList.toArray());
+ }
+ break;
+ default:
+ Log.e("EventLogParser", //$NON-NLS-1$
+ String.format("Unknown binary event type %1$d", type)); //$NON-NLS-1$
+ return -1;
+ }
+
+ return offset - dataOffset;
+ }
+
+ private Object parseTextData(String data, int tagValue) {
+ // first, get the description of what we're supposed to parse
+ EventValueDescription[] desc = mValueDescriptionMap.get(tagValue);
+
+ if (desc == null) {
+ // TODO parse and create string values.
+ return null;
+ }
+
+ if (desc.length == 1) {
+ return getObjectFromString(data, desc[0].getEventValueType());
+ } else if (data.startsWith("[") && data.endsWith("]")) {
+ data = data.substring(1, data.length() - 1);
+
+ // get each individual values as String
+ String[] values = data.split(",");
+
+ if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+ // special case for the GC event!
+ Object[] objects = new Object[2];
+
+ objects[0] = getObjectFromString(values[0], EventValueType.LONG);
+ objects[1] = getObjectFromString(values[1], EventValueType.LONG);
+
+ return objects;
+ } else {
+ // must be the same number as the number of descriptors.
+ if (values.length != desc.length) {
+ return null;
+ }
+
+ Object[] objects = new Object[values.length];
+
+ for (int i = 0 ; i < desc.length ; i++) {
+ Object obj = getObjectFromString(values[i], desc[i].getEventValueType());
+ if (obj == null) {
+ return null;
+ }
+ objects[i] = obj;
+ }
+
+ return objects;
+ }
+ }
+
+ return null;
+ }
+
+
+ private Object getObjectFromString(String value, EventValueType type) {
+ try {
+ switch (type) {
+ case INT:
+ return Integer.valueOf(value);
+ case LONG:
+ return Long.valueOf(value);
+ case STRING:
+ return value;
+ }
+ } catch (NumberFormatException e) {
+ // do nothing, we'll return null.
+ }
+
+ return null;
+ }
+
+ /**
+ * Recreates the event-log-tags at the specified file path.
+ * @param filePath the file path to write the file.
+ * @throws IOException
+ */
+ public void saveTags(String filePath) throws IOException {
+ File destFile = new File(filePath);
+ destFile.createNewFile();
+ FileOutputStream fos = null;
+
+ try {
+
+ fos = new FileOutputStream(destFile);
+
+ for (Integer key : mTagMap.keySet()) {
+ // get the tag name
+ String tagName = mTagMap.get(key);
+
+ // get the value descriptions
+ EventValueDescription[] descriptors = mValueDescriptionMap.get(key);
+
+ String line = null;
+ if (descriptors != null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$
+ boolean first = true;
+ for (EventValueDescription evd : descriptors) {
+ if (first) {
+ sb.append(" ("); //$NON-NLS-1$
+ first = false;
+ } else {
+ sb.append(",("); //$NON-NLS-1$
+ }
+ sb.append(evd.getName());
+ sb.append("|"); //$NON-NLS-1$
+ sb.append(evd.getEventValueType().getValue());
+ sb.append("|"); //$NON-NLS-1$
+ sb.append(evd.getValueType().getValue());
+ sb.append("|)"); //$NON-NLS-1$
+ }
+ sb.append("\n"); //$NON-NLS-1$
+
+ line = sb.toString();
+ } else {
+ line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$
+ }
+
+ byte[] buffer = line.getBytes();
+ fos.write(buffer);
+ }
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ }
+
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java
new file mode 100644
index 0000000..b68b4e8
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import com.android.ddmlib.log.EventContainer.EventValueType;
+
+
+/**
+ * Describes an {@link EventContainer} value.
+ * <p/>
+ * This is a stand-alone object, not linked to a particular Event. It describes the value, by
+ * name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}).
+ * <p/>
+ * The index of the value is not contained within this class, and is instead dependent on the
+ * index of this particular object in the array of {@link EventValueDescription} returned by
+ * {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag.
+ *
+ */
+public final class EventValueDescription {
+
+ /**
+ * Represents the type of a numerical value. This is used to display values of vastly different
+ * type/range in graphs.
+ */
+ public static enum ValueType {
+ NOT_APPLICABLE(0),
+ OBJECTS(1),
+ BYTES(2),
+ MILLISECONDS(3),
+ ALLOCATIONS(4),
+ ID(5),
+ PERCENT(6);
+
+ private int mValue;
+
+ /**
+ * Checks that the {@link EventValueType} is compatible with the {@link ValueType}.
+ * @param type the {@link EventValueType} to check.
+ * @throws InvalidValueTypeException if the types are not compatible.
+ */
+ public void checkType(EventValueType type) throws InvalidValueTypeException {
+ if ((type != EventValueType.INT && type != EventValueType.LONG)
+ && this != NOT_APPLICABLE) {
+ throw new InvalidValueTypeException(
+ String.format("%1$s doesn't support type %2$s", type, this));
+ }
+ }
+
+ /**
+ * Returns a {@link ValueType} from an integer value, or <code>null</code> if no match
+ * were found.
+ * @param value the integer value.
+ */
+ public static ValueType getValueType(int value) {
+ for (ValueType type : values()) {
+ if (type.mValue == value) {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the integer value of the enum.
+ */
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ private ValueType(int value) {
+ mValue = value;
+ }
+ }
+
+ private String mName;
+ private EventValueType mEventValueType;
+ private ValueType mValueType;
+
+ /**
+ * Builds a {@link EventValueDescription} with a name and a type.
+ * <p/>
+ * If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the
+ * {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to
+ * {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values.
+ * @param name
+ * @param type
+ */
+ EventValueDescription(String name, EventValueType type) {
+ mName = name;
+ mEventValueType = type;
+ if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) {
+ mValueType = ValueType.BYTES;
+ } else {
+ mValueType = ValueType.NOT_APPLICABLE;
+ }
+ }
+
+ /**
+ * Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}.
+ * <p/>
+ * @param name
+ * @param type
+ * @param valueType
+ * @throws InvalidValueTypeException if type and valuetype are not compatible.
+ *
+ */
+ EventValueDescription(String name, EventValueType type, ValueType valueType)
+ throws InvalidValueTypeException {
+ mName = name;
+ mEventValueType = type;
+ mValueType = valueType;
+ mValueType.checkType(mEventValueType);
+ }
+
+ /**
+ * @return the Name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * @return the {@link EventValueType}.
+ */
+ public EventValueType getEventValueType() {
+ return mEventValueType;
+ }
+
+ /**
+ * @return the {@link ValueType}.
+ */
+ public ValueType getValueType() {
+ return mValueType;
+ }
+
+ @Override
+ public String toString() {
+ if (mValueType != ValueType.NOT_APPLICABLE) {
+ return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(),
+ mValueType.toString());
+ }
+
+ return String.format("%1$s (%2$s)", mName, mEventValueType.toString());
+ }
+
+ /**
+ * Checks if the value is of the proper type for this receiver.
+ * @param value the value to check.
+ * @return true if the value is of the proper type for this receiver.
+ */
+ public boolean checkForType(Object value) {
+ switch (mEventValueType) {
+ case INT:
+ return value instanceof Integer;
+ case LONG:
+ return value instanceof Long;
+ case STRING:
+ return value instanceof String;
+ case LIST:
+ return value instanceof Object[];
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an object of a valid type (based on the value returned by
+ * {@link #getEventValueType()}) from a String value.
+ * <p/>
+ * IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not
+ * supported.
+ * @param value the value of the object expressed as a string.
+ * @return an object or null if the conversion could not be done.
+ */
+ public Object getObjectFromString(String value) {
+ switch (mEventValueType) {
+ case INT:
+ try {
+ return Integer.valueOf(value);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ case LONG:
+ try {
+ return Long.valueOf(value);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ case STRING:
+ return value;
+ }
+
+ return null;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java
new file mode 100644
index 0000000..7bae202
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+
+/**
+ * Custom Event Container for the Gc event since this event doesn't simply output data in
+ * int or long format, but encodes several values on 4 longs.
+ * <p/>
+ * The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must
+ * be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used.
+ */
+final class GcEventContainer extends EventContainer {
+
+ public final static int GC_EVENT_TAG = 20001;
+
+ private String processId;
+ private long gcTime;
+ private long bytesFreed;
+ private long objectsFreed;
+ private long actualSize;
+ private long allowedSize;
+ private long softLimit;
+ private long objectsAllocated;
+ private long bytesAllocated;
+ private long zActualSize;
+ private long zAllowedSize;
+ private long zObjectsAllocated;
+ private long zBytesAllocated;
+ private long dlmallocFootprint;
+ private long mallinfoTotalAllocatedSpace;
+ private long externalLimit;
+ private long externalBytesAllocated;
+
+ GcEventContainer(LogEntry entry, int tag, Object data) {
+ super(entry, tag, data);
+ init(data);
+ }
+
+ GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
+ super(tag, pid, tid, sec, nsec, data);
+ init(data);
+ }
+
+ /**
+ * @param data
+ */
+ private void init(Object data) {
+ if (data instanceof Object[]) {
+ Object[] values = (Object[])data;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] instanceof Long) {
+ parseDvmHeapInfo((Long)values[i], i);
+ }
+ }
+ }
+ }
+
+ @Override
+ public EventValueType getType() {
+ return EventValueType.LIST;
+ }
+
+ @Override
+ public boolean testValue(int index, Object value, CompareMethod compareMethod)
+ throws InvalidTypeException {
+ // do a quick easy check on the type.
+ if (index == 0) {
+ if ((value instanceof String) == false) {
+ throw new InvalidTypeException();
+ }
+ } else if ((value instanceof Long) == false) {
+ throw new InvalidTypeException();
+ }
+
+ switch (compareMethod) {
+ case EQUAL_TO:
+ if (index == 0) {
+ return processId.equals(value);
+ } else {
+ return getValueAsLong(index) == ((Long)value).longValue();
+ }
+ case LESSER_THAN:
+ return getValueAsLong(index) <= ((Long)value).longValue();
+ case LESSER_THAN_STRICT:
+ return getValueAsLong(index) < ((Long)value).longValue();
+ case GREATER_THAN:
+ return getValueAsLong(index) >= ((Long)value).longValue();
+ case GREATER_THAN_STRICT:
+ return getValueAsLong(index) > ((Long)value).longValue();
+ case BIT_CHECK:
+ return (getValueAsLong(index) & ((Long)value).longValue()) != 0;
+ }
+
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ @Override
+ public Object getValue(int valueIndex) {
+ if (valueIndex == 0) {
+ return processId;
+ }
+
+ try {
+ return new Long(getValueAsLong(valueIndex));
+ } catch (InvalidTypeException e) {
+ // this would only happened if valueIndex was 0, which we test above.
+ }
+
+ return null;
+ }
+
+ @Override
+ public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
+ return (double)getValueAsLong(valueIndex);
+ }
+
+ @Override
+ public String getValueAsString(int valueIndex) {
+ switch (valueIndex) {
+ case 0:
+ return processId;
+ default:
+ try {
+ return Long.toString(getValueAsLong(valueIndex));
+ } catch (InvalidTypeException e) {
+ // we shouldn't stop there since we test, in this method first.
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ /**
+ * Returns a custom array of {@link EventValueDescription} since the actual content of this
+ * event (list of (long, long) does not match the values encoded into those longs.
+ */
+ static EventValueDescription[] getValueDescriptions() {
+ try {
+ return new EventValueDescription[] {
+ new EventValueDescription("Process Name", EventValueType.STRING),
+ new EventValueDescription("GC Time", EventValueType.LONG,
+ ValueType.MILLISECONDS),
+ new EventValueDescription("Freed Objects", EventValueType.LONG,
+ ValueType.OBJECTS),
+ new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES),
+ new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES),
+ new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Allocated Objects (aggregate)",
+ EventValueType.LONG, ValueType.OBJECTS),
+ new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES),
+ new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES),
+ new EventValueDescription("Allocated Objects", EventValueType.LONG,
+ ValueType.OBJECTS),
+ new EventValueDescription("Allocated Bytes", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Actual Size (zygote)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG,
+ ValueType.OBJECTS),
+ new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("External Allocation Limit", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("External Bytes Allocated", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("dlmalloc Footprint", EventValueType.LONG,
+ ValueType.BYTES),
+ new EventValueDescription("Malloc Info: Total Allocated Space",
+ EventValueType.LONG, ValueType.BYTES),
+ };
+ } catch (InvalidValueTypeException e) {
+ // this shouldn't happen since we control manual the EventValueType and the ValueType
+ // values. For development purpose, we assert if this happens.
+ assert false;
+ }
+
+ // this shouldn't happen, but the compiler complains otherwise.
+ return null;
+ }
+
+ private void parseDvmHeapInfo(long data, int index) {
+ switch (index) {
+ case 0:
+ // [63 ] Must be zero
+ // [62-24] ASCII process identifier
+ // [23-12] GC time in ms
+ // [11- 0] Bytes freed
+
+ gcTime = float12ToInt((int)((data >> 12) & 0xFFFL));
+ bytesFreed = float12ToInt((int)(data & 0xFFFL));
+
+ // convert the long into an array, in the proper order so that we can convert the
+ // first 5 char into a string.
+ byte[] dataArray = new byte[8];
+ put64bitsToArray(data, dataArray, 0);
+
+ // get the name from the string
+ processId = new String(dataArray, 0, 5);
+ break;
+ case 1:
+ // [63-62] 10
+ // [61-60] Reserved; must be zero
+ // [59-48] Objects freed
+ // [47-36] Actual size (current footprint)
+ // [35-24] Allowed size (current hard max)
+ // [23-12] Objects allocated
+ // [11- 0] Bytes allocated
+ objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL));
+ actualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
+ allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
+ objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
+ bytesAllocated = float12ToInt((int)(data & 0xFFFL));
+ break;
+ case 2:
+ // [63-62] 11
+ // [61-60] Reserved; must be zero
+ // [59-48] Soft limit (current soft max)
+ // [47-36] Actual size (current footprint)
+ // [35-24] Allowed size (current hard max)
+ // [23-12] Objects allocated
+ // [11- 0] Bytes allocated
+ softLimit = float12ToInt((int)((data >> 48) & 0xFFFL));
+ zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
+ zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
+ zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
+ zBytesAllocated = float12ToInt((int)(data & 0xFFFL));
+ break;
+ case 3:
+ // [63-48] Reserved; must be zero
+ // [47-36] dlmallocFootprint
+ // [35-24] mallinfo: total allocated space
+ // [23-12] External byte limit
+ // [11- 0] External bytes allocated
+ dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL));
+ mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL));
+ externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL));
+ externalBytesAllocated = float12ToInt((int)(data & 0xFFFL));
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Converts a 12 bit float representation into an unsigned int (returned as a long)
+ * @param f12
+ */
+ private static long float12ToInt(int f12) {
+ return (f12 & 0x1FF) << ((f12 >>> 9) * 4);
+ }
+
+ /**
+ * puts an unsigned value in an array.
+ * @param value The value to put.
+ * @param dest the destination array
+ * @param offset the offset in the array where to put the value.
+ * Array length must be at least offset + 8
+ */
+ private static void put64bitsToArray(long value, byte[] dest, int offset) {
+ dest[offset + 7] = (byte)(value & 0x00000000000000FFL);
+ dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8);
+ dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16);
+ dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24);
+ dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32);
+ dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40);
+ dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48);
+ dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56);
+ }
+
+ /**
+ * Returns the long value of the <code>valueIndex</code>-th value.
+ * @param valueIndex the index of the value.
+ * @throws InvalidTypeException if index is 0 as it is a string value.
+ */
+ private final long getValueAsLong(int valueIndex) throws InvalidTypeException {
+ switch (valueIndex) {
+ case 0:
+ throw new InvalidTypeException();
+ case 1:
+ return gcTime;
+ case 2:
+ return objectsFreed;
+ case 3:
+ return bytesFreed;
+ case 4:
+ return softLimit;
+ case 5:
+ return actualSize;
+ case 6:
+ return allowedSize;
+ case 7:
+ return objectsAllocated;
+ case 8:
+ return bytesAllocated;
+ case 9:
+ return actualSize - zActualSize;
+ case 10:
+ return allowedSize - zAllowedSize;
+ case 11:
+ return objectsAllocated - zObjectsAllocated;
+ case 12:
+ return bytesAllocated - zBytesAllocated;
+ case 13:
+ return zActualSize;
+ case 14:
+ return zAllowedSize;
+ case 15:
+ return zObjectsAllocated;
+ case 16:
+ return zBytesAllocated;
+ case 17:
+ return externalLimit;
+ case 18:
+ return externalBytesAllocated;
+ case 19:
+ return dlmallocFootprint;
+ case 20:
+ return mallinfoTotalAllocatedSpace;
+ }
+
+ throw new ArrayIndexOutOfBoundsException();
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java
new file mode 100644
index 0000000..016f8aa
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import java.io.Serializable;
+
+/**
+ * Exception thrown when accessing an {@link EventContainer} value with the wrong type.
+ */
+public final class InvalidTypeException extends Exception {
+
+ /**
+ * Needed by {@link Serializable}.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with the default detail message.
+ * @see java.lang.Exception
+ */
+ public InvalidTypeException() {
+ super("Invalid Type");
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ * @param message the detail message. The detail message is saved for later retrieval
+ * by the {@link Throwable#getMessage()} method.
+ * @see java.lang.Exception
+ */
+ public InvalidTypeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of
+ * <code>(cause==null ? null : cause.toString())</code> (which typically contains
+ * the class and detail message of cause).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+ * and indicates that the cause is nonexistent or unknown.)
+ * @see java.lang.Exception
+ */
+ public InvalidTypeException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ * @param message the detail message. The detail message is saved for later retrieval
+ * by the {@link Throwable#getMessage()} method.
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+ * and indicates that the cause is nonexistent or unknown.)
+ * @see java.lang.Exception
+ */
+ public InvalidTypeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java
new file mode 100644
index 0000000..a3050c8
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+
+import java.io.Serializable;
+
+/**
+ * Exception thrown when associating an {@link EventValueType} with an incompatible
+ * {@link ValueType}.
+ */
+public final class InvalidValueTypeException extends Exception {
+
+ /**
+ * Needed by {@link Serializable}.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with the default detail message.
+ * @see java.lang.Exception
+ */
+ public InvalidValueTypeException() {
+ super("Invalid Type");
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ * @param message the detail message. The detail message is saved for later retrieval
+ * by the {@link Throwable#getMessage()} method.
+ * @see java.lang.Exception
+ */
+ public InvalidValueTypeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of
+ * <code>(cause==null ? null : cause.toString())</code> (which typically contains
+ * the class and detail message of cause).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+ * and indicates that the cause is nonexistent or unknown.)
+ * @see java.lang.Exception
+ */
+ public InvalidValueTypeException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ * @param message the detail message. The detail message is saved for later retrieval
+ * by the {@link Throwable#getMessage()} method.
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+ * and indicates that the cause is nonexistent or unknown.)
+ * @see java.lang.Exception
+ */
+ public InvalidValueTypeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java
new file mode 100644
index 0000000..b49f025
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.log;
+
+
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.security.InvalidParameterException;
+
+/**
+ * Receiver able to provide low level parsing for device-side log services.
+ */
+public final class LogReceiver {
+
+ private final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry.
+
+ /**
+ * Represents a log entry and its raw data.
+ */
+ public final static class LogEntry {
+ /*
+ * See //device/include/utils/logger.h
+ */
+ /** 16bit unsigned: length of the payload. */
+ public int len; /* This is normally followed by a 16 bit padding */
+ /** pid of the process that generated this {@link LogEntry} */
+ public int pid;
+ /** tid of the process that generated this {@link LogEntry} */
+ public int tid;
+ /** Seconds since epoch. */
+ public int sec;
+ /** nanoseconds. */
+ public int nsec;
+ /** The entry's raw data. */
+ public byte[] data;
+ };
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}.
+ * <p/>This interface provides two methods.
+ * <ul>
+ * <li>{@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a
+ * first level of parsing, extracting {@link LogEntry} objects out of the log service output.</li>
+ * <li>{@link #newData(byte[], int, int)} provides a way to receive the raw information
+ * coming directly from the log service.</li>
+ * </ul>
+ */
+ public interface ILogListener {
+ /**
+ * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}.
+ * @param entry the new log entry.
+ */
+ public void newEntry(LogEntry entry);
+
+ /**
+ * Sent when new raw data is coming from the log service.
+ * @param data the raw data buffer.
+ * @param offset the offset into the buffer signaling the beginning of the new data.
+ * @param length the length of the new data.
+ */
+ public void newData(byte[] data, int offset, int length);
+ }
+
+ /** Current {@link LogEntry} being read, before sending it to the listener. */
+ private LogEntry mCurrentEntry;
+
+ /** Temp buffer to store partial entry headers. */
+ private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE];
+ /** Offset in the partial header buffer */
+ private int mEntryHeaderOffset = 0;
+ /** Offset in the partial entry data */
+ private int mEntryDataOffset = 0;
+
+ /** Listener waiting for receive fully read {@link LogEntry} objects */
+ private ILogListener mListener;
+
+ private boolean mIsCancelled = false;
+
+ /**
+ * Creates a {@link LogReceiver} with an {@link ILogListener}.
+ * <p/>
+ * The {@link ILogListener} will receive new log entries as they are parsed, in the form
+ * of {@link LogEntry} objects.
+ * @param listener the listener to receive new log entries.
+ */
+ public LogReceiver(ILogListener listener) {
+ mListener = listener;
+ }
+
+
+ /**
+ * Parses new data coming from the log service.
+ * @param data the data buffer
+ * @param offset the offset into the buffer signaling the beginning of the new data.
+ * @param length the length of the new data.
+ */
+ public void parseNewData(byte[] data, int offset, int length) {
+ // notify the listener of new raw data
+ if (mListener != null) {
+ mListener.newData(data, offset, length);
+ }
+
+ // loop while there is still data to be read and the receiver has not be cancelled.
+ while (length > 0 && mIsCancelled == false) {
+ // first check if we have no current entry.
+ if (mCurrentEntry == null) {
+ if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) {
+ // if we don't have enough data to finish the header, save
+ // the data we have and return
+ System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length);
+ mEntryHeaderOffset += length;
+ return;
+ } else {
+ // we have enough to fill the header, let's do it.
+ // did we store some part at the beginning of the header?
+ if (mEntryHeaderOffset != 0) {
+ // copy the rest of the entry header into the header buffer
+ int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset;
+ System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset,
+ size);
+
+ // create the entry from the header buffer
+ mCurrentEntry = createEntry(mEntryHeaderBuffer, 0);
+
+ // since we used the whole entry header buffer, we reset the offset
+ mEntryHeaderOffset = 0;
+
+ // adjust current offset and remaining length to the beginning
+ // of the entry data
+ offset += size;
+ length -= size;
+ } else {
+ // create the entry directly from the data array
+ mCurrentEntry = createEntry(data, offset);
+
+ // adjust current offset and remaining length to the beginning
+ // of the entry data
+ offset += ENTRY_HEADER_SIZE;
+ length -= ENTRY_HEADER_SIZE;
+ }
+ }
+ }
+
+ // at this point, we have an entry, and offset/length have been updated to skip
+ // the entry header.
+
+ // if we have enough data for this entry or more, we'll need to end this entry
+ if (length >= mCurrentEntry.len - mEntryDataOffset) {
+ // compute and save the size of the data that we have to read for this entry,
+ // based on how much we may already have read.
+ int dataSize = mCurrentEntry.len - mEntryDataOffset;
+
+ // we only read what we need, and put it in the entry buffer.
+ System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize);
+
+ // notify the listener of a new entry
+ if (mListener != null) {
+ mListener.newEntry(mCurrentEntry);
+ }
+
+ // reset some flags: we have read 0 data of the current entry.
+ // and we have no current entry being read.
+ mEntryDataOffset = 0;
+ mCurrentEntry = null;
+
+ // and update the data buffer info to the end of the current entry / start
+ // of the next one.
+ offset += dataSize;
+ length -= dataSize;
+ } else {
+ // we don't have enough data to fill this entry, so we store what we have
+ // in the entry itself.
+ System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length);
+
+ // save the amount read for the data.
+ mEntryDataOffset += length;
+ return;
+ }
+ }
+ }
+
+ /**
+ * Returns whether this receiver is canceling the remote service.
+ */
+ public boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ /**
+ * Cancels the current remote service.
+ */
+ public void cancel() {
+ mIsCancelled = true;
+ }
+
+ /**
+ * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size
+ * to be at least <code>offset + {@link #ENTRY_HEADER_SIZE}</code>.
+ * @param data the data buffer the entry is read from.
+ * @param offset the offset of the first byte from the buffer representing the entry.
+ * @return a new {@link LogEntry} or <code>null</code> if some error happened.
+ */
+ private LogEntry createEntry(byte[] data, int offset) {
+ if (data.length < offset + ENTRY_HEADER_SIZE) {
+ throw new InvalidParameterException(
+ "Buffer not big enough to hold full LoggerEntry header");
+ }
+
+ // create the new entry and fill it.
+ LogEntry entry = new LogEntry();
+ entry.len = ArrayHelper.swapU16bitFromArray(data, offset);
+
+ // we've read only 16 bits, but since there's also a 16 bit padding,
+ // we can skip right over both.
+ offset += 4;
+
+ entry.pid = ArrayHelper.swap32bitFromArray(data, offset);
+ offset += 4;
+ entry.tid = ArrayHelper.swap32bitFromArray(data, offset);
+ offset += 4;
+ entry.sec = ArrayHelper.swap32bitFromArray(data, offset);
+ offset += 4;
+ entry.nsec = ArrayHelper.swap32bitFromArray(data, offset);
+ offset += 4;
+
+ // allocate the data
+ entry.data = new byte[entry.len];
+
+ return entry;
+ }
+
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java
new file mode 100644
index 0000000..b61a698
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+/**
+ * Receives event notifications during instrumentation test runs.
+ * Patterned after {@link junit.runner.TestRunListener}.
+ */
+public interface ITestRunListener {
+
+ /**
+ * Types of test failures.
+ */
+ enum TestFailure {
+ /** Test failed due to unanticipated uncaught exception. */
+ ERROR,
+ /** Test failed due to a false assertion. */
+ FAILURE
+ }
+
+ /**
+ * Reports the start of a test run.
+ *
+ * @param testCount total number of tests in test run
+ */
+ public void testRunStarted(int testCount);
+
+ /**
+ * Reports end of test run.
+ *
+ * @param elapsedTime device reported elapsed time, in milliseconds
+ */
+ public void testRunEnded(long elapsedTime);
+
+ /**
+ * Reports test run stopped before completion.
+ *
+ * @param elapsedTime device reported elapsed time, in milliseconds
+ */
+ public void testRunStopped(long elapsedTime);
+
+ /**
+ * Reports the start of an individual test case.
+ *
+ * @param test identifies the test
+ */
+ public void testStarted(TestIdentifier test);
+
+ /**
+ * Reports the execution end of an individual test case.
+ * If {@link #testFailed} was not invoked, this test passed.
+ *
+ * @param test identifies the test
+ */
+ public void testEnded(TestIdentifier test);
+
+ /**
+ * Reports the failure of a individual test case.
+ * Will be called between testStarted and testEnded.
+ *
+ * @param status failure type
+ * @param test identifies the test
+ * @param trace stack trace of failure
+ */
+ public void testFailed(TestFailure status, TestIdentifier test, String trace);
+
+ /**
+ * Reports test run failed to execute due to a fatal error.
+ */
+ public void testRunFailed(String errorMessage);
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
new file mode 100755
index 0000000..bc1834f
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+
+/**
+ * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
+ * ITestRunListener of the results.
+ *
+ * <p>Expects the following output:
+ *
+ * <p>If fatal error occurred when attempted to run the tests:
+ * <pre> INSTRUMENTATION_FAILED: </pre>
+ *
+ * <p>Otherwise, expect a series of test results, each one containing a set of status key/value
+ * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
+ * run, expects that the elapsed test time in seconds will be displayed
+ *
+ * <p>For example:
+ * <pre>
+ * INSTRUMENTATION_STATUS_CODE: 1
+ * INSTRUMENTATION_STATUS: class=com.foo.FooTest
+ * INSTRUMENTATION_STATUS: test=testFoo
+ * INSTRUMENTATION_STATUS: numtests=2
+ * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
+ * com.foo.X
+ * INSTRUMENTATION_STATUS_CODE: -2
+ * ...
+ *
+ * Time: X
+ * </pre>
+ * <p>Note that the "value" portion of the key-value pair may wrap over several text lines
+ */
+public class InstrumentationResultParser extends MultiLineReceiver {
+
+ /** Relevant test status keys. */
+ private static class StatusKeys {
+ private static final String TEST = "test";
+ private static final String CLASS = "class";
+ private static final String STACK = "stack";
+ private static final String NUMTESTS = "numtests";
+ }
+
+ /** Test result status codes. */
+ private static class StatusCodes {
+ private static final int FAILURE = -2;
+ private static final int START = 1;
+ private static final int ERROR = -1;
+ private static final int OK = 0;
+ }
+
+ /** Prefixes used to identify output. */
+ private static class Prefixes {
+ private static final String STATUS = "INSTRUMENTATION_STATUS: ";
+ private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
+ private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
+ private static final String TIME_REPORT = "Time: ";
+ }
+
+ private final ITestRunListener mTestListener;
+
+ /**
+ * Test result data
+ */
+ private static class TestResult {
+ private Integer mCode = null;
+ private String mTestName = null;
+ private String mTestClass = null;
+ private String mStackTrace = null;
+ private Integer mNumTests = null;
+
+ /** Returns true if all expected values have been parsed */
+ boolean isComplete() {
+ return mCode != null && mTestName != null && mTestClass != null;
+ }
+ }
+
+ /** Stores the status values for the test result currently being parsed */
+ private TestResult mCurrentTestResult = null;
+
+ /** Stores the current "key" portion of the status key-value being parsed. */
+ private String mCurrentKey = null;
+
+ /** Stores the current "value" portion of the status key-value being parsed. */
+ private StringBuilder mCurrentValue = null;
+
+ /** True if start of test has already been reported to listener. */
+ private boolean mTestStartReported = false;
+
+ /** The elapsed time of the test run, in milliseconds. */
+ private long mTestTime = 0;
+
+ /** True if current test run has been canceled by user. */
+ private boolean mIsCancelled = false;
+
+ private static final String LOG_TAG = "InstrumentationResultParser";
+
+ /**
+ * Creates the InstrumentationResultParser.
+ *
+ * @param listener informed of test results as the tests are executing
+ */
+ public InstrumentationResultParser(ITestRunListener listener) {
+ mTestListener = listener;
+ }
+
+ /**
+ * Processes the instrumentation test output from shell.
+ *
+ * @see MultiLineReceiver#processNewLines
+ */
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ parse(line);
+ }
+ }
+
+ /**
+ * Parse an individual output line. Expects a line that is one of:
+ * <ul>
+ * <li>
+ * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
+ * and thus there is a new key=value pair to parse, and the previous key-value pair is
+ * finished.
+ * </li>
+ * <li>
+ * A continuation of the previous status (the "value" portion of the key has wrapped
+ * to the next line).
+ * </li>
+ * <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li>
+ * <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li>
+ * </ul>
+ *
+ * @param line Text output line
+ */
+ private void parse(String line) {
+ if (line.startsWith(Prefixes.STATUS_CODE)) {
+ // Previous status key-value has been collected. Store it.
+ submitCurrentKeyValue();
+ parseStatusCode(line);
+ } else if (line.startsWith(Prefixes.STATUS)) {
+ // Previous status key-value has been collected. Store it.
+ submitCurrentKeyValue();
+ parseKey(line, Prefixes.STATUS.length());
+ } else if (line.startsWith(Prefixes.STATUS_FAILED)) {
+ Log.e(LOG_TAG, "test run failed " + line);
+ mTestListener.testRunFailed(line);
+ } else if (line.startsWith(Prefixes.TIME_REPORT)) {
+ parseTime(line, Prefixes.TIME_REPORT.length());
+ } else {
+ if (mCurrentValue != null) {
+ // this is a value that has wrapped to next line.
+ mCurrentValue.append("\r\n");
+ mCurrentValue.append(line);
+ } else {
+ Log.w(LOG_TAG, "unrecognized line " + line);
+ }
+ }
+ }
+
+ /**
+ * Stores the currently parsed key-value pair into mCurrentTestInfo.
+ */
+ private void submitCurrentKeyValue() {
+ if (mCurrentKey != null && mCurrentValue != null) {
+ TestResult testInfo = getCurrentTestInfo();
+ String statusValue = mCurrentValue.toString();
+
+ if (mCurrentKey.equals(StatusKeys.CLASS)) {
+ testInfo.mTestClass = statusValue.trim();
+ }
+ else if (mCurrentKey.equals(StatusKeys.TEST)) {
+ testInfo.mTestName = statusValue.trim();
+ }
+ else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
+ try {
+ testInfo.mNumTests = Integer.parseInt(statusValue);
+ }
+ catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue);
+ }
+ }
+ else if (mCurrentKey.equals(StatusKeys.STACK)) {
+ testInfo.mStackTrace = statusValue;
+ }
+
+ mCurrentKey = null;
+ mCurrentValue = null;
+ }
+ }
+
+ private TestResult getCurrentTestInfo() {
+ if (mCurrentTestResult == null) {
+ mCurrentTestResult = new TestResult();
+ }
+ return mCurrentTestResult;
+ }
+
+ private void clearCurrentTestInfo() {
+ mCurrentTestResult = null;
+ }
+
+ /**
+ * Parses the key from the current line.
+ * Expects format of "key=value".
+ *
+ * @param line full line of text to parse
+ * @param keyStartPos the starting position of the key in the given line
+ */
+ private void parseKey(String line, int keyStartPos) {
+ int endKeyPos = line.indexOf('=', keyStartPos);
+ if (endKeyPos != -1) {
+ mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
+ parseValue(line, endKeyPos+1);
+ }
+ }
+
+ /**
+ * Parses the start of a key=value pair.
+ *
+ * @param line - full line of text to parse
+ * @param valueStartPos - the starting position of the value in the given line
+ */
+ private void parseValue(String line, int valueStartPos) {
+ mCurrentValue = new StringBuilder();
+ mCurrentValue.append(line.substring(valueStartPos));
+ }
+
+ /**
+ * Parses out a status code result.
+ */
+ private void parseStatusCode(String line) {
+ String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
+ TestResult testInfo = getCurrentTestInfo();
+ try {
+ testInfo.mCode = Integer.parseInt(value);
+ }
+ catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Expected integer status code, received: " + value);
+ }
+
+ // this means we're done with current test result bundle
+ reportResult(testInfo);
+ clearCurrentTestInfo();
+ }
+
+ /**
+ * Returns true if test run canceled.
+ *
+ * @see IShellOutputReceiver#isCancelled()
+ */
+ public boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ /**
+ * Requests cancellation of test run.
+ */
+ public void cancel() {
+ mIsCancelled = true;
+ }
+
+ /**
+ * Reports a test result to the test run listener. Must be called when a individual test
+ * result has been fully parsed.
+ *
+ * @param statusMap key-value status pairs of test result
+ */
+ private void reportResult(TestResult testInfo) {
+ if (!testInfo.isComplete()) {
+ Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
+ return;
+ }
+ reportTestRunStarted(testInfo);
+ TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
+
+ switch (testInfo.mCode) {
+ case StatusCodes.START:
+ mTestListener.testStarted(testId);
+ break;
+ case StatusCodes.FAILURE:
+ mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
+ getTrace(testInfo));
+ mTestListener.testEnded(testId);
+ break;
+ case StatusCodes.ERROR:
+ mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+ getTrace(testInfo));
+ mTestListener.testEnded(testId);
+ break;
+ case StatusCodes.OK:
+ mTestListener.testEnded(testId);
+ break;
+ default:
+ Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
+ mTestListener.testEnded(testId);
+ break;
+ }
+
+ }
+
+ /**
+ * Reports the start of a test run, and the total test count, if it has not been previously
+ * reported.
+ *
+ * @param testInfo current test status values
+ */
+ private void reportTestRunStarted(TestResult testInfo) {
+ // if start test run not reported yet
+ if (!mTestStartReported && testInfo.mNumTests != null) {
+ mTestListener.testRunStarted(testInfo.mNumTests);
+ mTestStartReported = true;
+ }
+ }
+
+ /**
+ * Returns the stack trace of the current failed test, from the provided testInfo.
+ */
+ private String getTrace(TestResult testInfo) {
+ if (testInfo.mStackTrace != null) {
+ return testInfo.mStackTrace;
+ }
+ else {
+ Log.e(LOG_TAG, "Could not find stack trace for failed test ");
+ return new Throwable("Unknown failure").toString();
+ }
+ }
+
+ /**
+ * Parses out and store the elapsed time.
+ */
+ private void parseTime(String line, int startPos) {
+ String timeString = line.substring(startPos);
+ try {
+ float timeSeconds = Float.parseFloat(timeString);
+ mTestTime = (long)(timeSeconds * 1000);
+ }
+ catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Unexpected time format " + timeString);
+ }
+ }
+
+ /**
+ * Called by parent when adb session is complete.
+ */
+ @Override
+ public void done() {
+ super.done();
+ mTestListener.testRunEnded(mTestTime);
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
new file mode 100644
index 0000000..4edbbbb
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+
+import java.io.IOException;
+
+/**
+ * Runs a Android test command remotely and reports results.
+ */
+public class RemoteAndroidTestRunner {
+
+ private static final char CLASS_SEPARATOR = ',';
+ private static final char METHOD_SEPARATOR = '#';
+ private static final char RUNNER_SEPARATOR = '/';
+ private String mClassArg;
+ private final String mPackageName;
+ private final String mRunnerName;
+ private String mExtraArgs;
+ private boolean mLogOnlyMode;
+ private IDevice mRemoteDevice;
+ private InstrumentationResultParser mParser;
+
+ private static final String LOG_TAG = "RemoteAndroidTest";
+ private static final String DEFAULT_RUNNER_NAME =
+ "android.test.InstrumentationTestRunner";
+
+ /**
+ * Creates a remote Android test runner.
+ *
+ * @param packageName the Android application package that contains the tests to run
+ * @param runnerName the instrumentation test runner to execute. If null, will use default
+ * runner
+ * @param remoteDevice the Android device to execute tests on
+ */
+ public RemoteAndroidTestRunner(String packageName,
+ String runnerName,
+ IDevice remoteDevice) {
+
+ mPackageName = packageName;
+ mRunnerName = runnerName;
+ mRemoteDevice = remoteDevice;
+ mClassArg = null;
+ mExtraArgs = "";
+ mLogOnlyMode = false;
+ }
+
+ /**
+ * Alternate constructor. Uses default instrumentation runner.
+ *
+ * @param packageName the Android application package that contains the tests to run
+ * @param remoteDevice the Android device to execute tests on
+ */
+ public RemoteAndroidTestRunner(String packageName,
+ IDevice remoteDevice) {
+ this(packageName, null, remoteDevice);
+ }
+
+ /**
+ * Returns the application package name.
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the runnerName.
+ */
+ public String getRunnerName() {
+ if (mRunnerName == null) {
+ return DEFAULT_RUNNER_NAME;
+ }
+ return mRunnerName;
+ }
+
+ /**
+ * Returns the complete instrumentation component path.
+ */
+ private String getRunnerPath() {
+ return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
+ }
+
+ /**
+ * Sets to run only tests in this class
+ * Must be called before 'run'.
+ *
+ * @param className fully qualified class name (eg x.y.z)
+ */
+ public void setClassName(String className) {
+ mClassArg = className;
+ }
+
+ /**
+ * Sets to run only tests in the provided classes
+ * Must be called before 'run'.
+ * <p>
+ * If providing more than one class, requires a InstrumentationTestRunner that supports
+ * the multiple class argument syntax.
+ *
+ * @param classNames array of fully qualified class names (eg x.y.z)
+ */
+ public void setClassNames(String[] classNames) {
+ StringBuilder classArgBuilder = new StringBuilder();
+
+ for (int i=0; i < classNames.length; i++) {
+ if (i != 0) {
+ classArgBuilder.append(CLASS_SEPARATOR);
+ }
+ classArgBuilder.append(classNames[i]);
+ }
+ mClassArg = classArgBuilder.toString();
+ }
+
+ /**
+ * Sets to run only specified test method
+ * Must be called before 'run'.
+ *
+ * @param className fully qualified class name (eg x.y.z)
+ * @param testName method name
+ */
+ public void setMethodName(String className, String testName) {
+ mClassArg = className + METHOD_SEPARATOR + testName;
+ }
+
+ /**
+ * Sets extra arguments to include in instrumentation command.
+ * Must be called before 'run'.
+ *
+ * @param instrumentationArgs must not be null
+ */
+ public void setExtraArgs(String instrumentationArgs) {
+ if (instrumentationArgs == null) {
+ throw new IllegalArgumentException("instrumentationArgs cannot be null");
+ }
+ mExtraArgs = instrumentationArgs;
+ }
+
+ /**
+ * Returns the extra instrumentation arguments.
+ */
+ public String getExtraArgs() {
+ return mExtraArgs;
+ }
+
+ /**
+ * Sets this test run to log only mode - skips test execution.
+ */
+ public void setLogOnly(boolean logOnly) {
+ mLogOnlyMode = logOnly;
+ }
+
+ /**
+ * Execute this test run.
+ *
+ * @param listener listens for test results
+ */
+ public void run(ITestRunListener listener) {
+ final String runCaseCommandStr = "am instrument -w -r "
+ + getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath();
+ Log.d(LOG_TAG, runCaseCommandStr);
+ mParser = new InstrumentationResultParser(listener);
+
+ try {
+ mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e);
+ listener.testRunFailed(e.toString());
+ }
+ }
+
+ /**
+ * Requests cancellation of this test run.
+ */
+ public void cancel() {
+ if (mParser != null) {
+ mParser.cancel();
+ }
+ }
+
+ /**
+ * Returns the test class argument.
+ */
+ private String getClassArg() {
+ return mClassArg;
+ }
+
+ /**
+ * Returns the full instrumentation command which specifies the test classes to execute.
+ * Returns an empty string if no classes were specified.
+ */
+ private String getClassCmd() {
+ String classArg = getClassArg();
+ if (classArg != null) {
+ return "-e class " + classArg;
+ }
+ return "";
+ }
+
+ /**
+ * Returns the full command to enable log only mode - if specified. Otherwise returns an
+ * empty string.
+ */
+ private String getLogCmd() {
+ if (mLogOnlyMode) {
+ return "-e log true";
+ }
+ else {
+ return "";
+ }
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java
new file mode 100644
index 0000000..4d3b108
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+/**
+ * Identifies a parsed instrumentation test
+ */
+public class TestIdentifier {
+
+ private final String mClassName;
+ private final String mTestName;
+
+ /**
+ * Creates a test identifier
+ *
+ * @param className fully qualified class name of the test. Cannot be null.
+ * @param testName name of the test. Cannot be null.
+ */
+ public TestIdentifier(String className, String testName) {
+ if (className == null || testName == null) {
+ throw new IllegalArgumentException("className and testName must " +
+ "be non-null");
+ }
+ mClassName = className;
+ mTestName = testName;
+ }
+
+ /**
+ * Returns the fully qualified class name of the test
+ */
+ public String getClassName() {
+ return mClassName;
+ }
+
+ /**
+ * Returns the name of the test
+ */
+ public String getTestName() {
+ return mTestName;
+ }
+
+ /**
+ * Tests equality by comparing class and method name
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof TestIdentifier)) {
+ return false;
+ }
+ TestIdentifier otherTest = (TestIdentifier)other;
+ return getClassName().equals(otherTest.getClassName()) &&
+ getTestName().equals(otherTest.getTestName());
+ }
+
+ /**
+ * Generates hashCode based on class and method name.
+ */
+ @Override
+ public int hashCode() {
+ return getClassName().hashCode() * 31 + getTestName().hashCode();
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java
new file mode 100644
index 0000000..8167e5d
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.utils;
+
+/**
+ * Utility class providing array to int/long conversion for data received from devices through adb.
+ */
+public final class ArrayHelper {
+
+ /**
+ * Swaps an unsigned value around, and puts the result in an array that can be sent to a device.
+ * @param value The value to swap.
+ * @param dest the destination array
+ * @param offset the offset in the array where to put the swapped value.
+ * Array length must be at least offset + 4
+ */
+ public static void swap32bitsToArray(int value, byte[] dest, int offset) {
+ dest[offset] = (byte)(value & 0x000000FF);
+ dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8);
+ dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16);
+ dest[offset + 3] = (byte)((value & 0xFF000000) >> 24);
+ }
+
+ /**
+ * Reads a signed 32 bit integer from an array coming from a device.
+ * @param value the array containing the int
+ * @param offset the offset in the array at which the int starts
+ * @return the integer read from the array
+ */
+ public static int swap32bitFromArray(byte[] value, int offset) {
+ int v = 0;
+ v |= ((int)value[offset]) & 0x000000FF;
+ v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
+ v |= (((int)value[offset + 2]) & 0x000000FF) << 16;
+ v |= (((int)value[offset + 3]) & 0x000000FF) << 24;
+
+ return v;
+ }
+
+ /**
+ * Reads an unsigned 16 bit integer from an array coming from a device,
+ * and returns it as an 'int'
+ * @param value the array containing the 16 bit int (2 byte).
+ * @param offset the offset in the array at which the int starts
+ * Array length must be at least offset + 2
+ * @return the integer read from the array.
+ */
+ public static int swapU16bitFromArray(byte[] value, int offset) {
+ int v = 0;
+ v |= ((int)value[offset]) & 0x000000FF;
+ v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
+
+ return v;
+ }
+
+ /**
+ * Reads a signed 64 bit integer from an array coming from a device.
+ * @param value the array containing the int
+ * @param offset the offset in the array at which the int starts
+ * Array length must be at least offset + 8
+ * @return the integer read from the array
+ */
+ public static long swap64bitFromArray(byte[] value, int offset) {
+ long v = 0;
+ v |= ((long)value[offset]) & 0x00000000000000FFL;
+ v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8;
+ v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16;
+ v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24;
+ v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32;
+ v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40;
+ v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48;
+ v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56;
+
+ return v;
+ }
+}
diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
new file mode 100644
index 0000000..77d10c1
--- /dev/null
+++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests InstrumentationResultParser.
+ */
+public class InstrumentationResultParserTest extends TestCase {
+
+ private InstrumentationResultParser mParser;
+ private VerifyingTestResult mTestResult;
+
+ // static dummy test names to use for validation
+ private static final String CLASS_NAME = "com.test.FooTest";
+ private static final String TEST_NAME = "testFoo";
+ private static final String STACK_TRACE = "java.lang.AssertionFailedException";
+
+ /**
+ * @param name - test name
+ */
+ public InstrumentationResultParserTest(String name) {
+ super(name);
+ }
+
+ /**
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTestResult = new VerifyingTestResult();
+ mParser = new InstrumentationResultParser(mTestResult);
+ }
+
+ /**
+ * Tests that the test run started and test start events is sent on first
+ * bundle received.
+ */
+ public void testTestStarted() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+ assertEquals(0, mTestResult.mNumTestsRun);
+ }
+
+ /**
+ * Tests that a single successful test execution.
+ */
+ public void testTestSuccess() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+ addCommonStatus(output);
+ addSuccessCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+ assertEquals(1, mTestResult.mNumTestsRun);
+ assertEquals(null, mTestResult.mTestStatus);
+ }
+
+ /**
+ * Test basic parsing of failed test case.
+ */
+ public void testTestFailed() {
+ StringBuilder output = buildCommonResult();
+ addStartCode(output);
+ addCommonStatus(output);
+ addStackTrace(output);
+ addFailureCode(output);
+
+ injectTestString(output.toString());
+ assertCommonAttributes();
+
+ assertEquals(1, mTestResult.mNumTestsRun);
+ assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus);
+ assertEquals(STACK_TRACE, mTestResult.mTrace);
+ }
+
+ /**
+ * Test basic parsing and conversion of time from output.
+ */
+ public void testTimeParsing() {
+ final String timeString = "Time: 4.9";
+ injectTestString(timeString);
+ assertEquals(4900, mTestResult.mTestTime);
+ }
+
+ /**
+ * builds a common test result using TEST_NAME and TEST_CLASS.
+ */
+ private StringBuilder buildCommonResult() {
+ StringBuilder output = new StringBuilder();
+ // add test start bundle
+ addCommonStatus(output);
+ addStatusCode(output, "1");
+ // add end test bundle, without status
+ addCommonStatus(output);
+ return output;
+ }
+
+ /**
+ * Adds common status results to the provided output.
+ */
+ private void addCommonStatus(StringBuilder output) {
+ addStatusKey(output, "stream", "\r\n" + CLASS_NAME);
+ addStatusKey(output, "test", TEST_NAME);
+ addStatusKey(output, "class", CLASS_NAME);
+ addStatusKey(output, "current", "1");
+ addStatusKey(output, "numtests", "1");
+ addStatusKey(output, "id", "InstrumentationTestRunner");
+ }
+
+ /**
+ * Adds a stack trace status bundle to output.
+ */
+ private void addStackTrace(StringBuilder output) {
+ addStatusKey(output, "stack", STACK_TRACE);
+
+ }
+
+ /**
+ * Helper method to add a status key-value bundle.
+ */
+ private void addStatusKey(StringBuilder outputBuilder, String key,
+ String value) {
+ outputBuilder.append("INSTRUMENTATION_STATUS: ");
+ outputBuilder.append(key);
+ outputBuilder.append('=');
+ outputBuilder.append(value);
+ outputBuilder.append("\r\n");
+ }
+
+ private void addStartCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "1");
+ }
+
+ private void addSuccessCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "0");
+ }
+
+ private void addFailureCode(StringBuilder outputBuilder) {
+ addStatusCode(outputBuilder, "-2");
+ }
+
+ private void addStatusCode(StringBuilder outputBuilder, String value) {
+ outputBuilder.append("INSTRUMENTATION_STATUS_CODE: ");
+ outputBuilder.append(value);
+ outputBuilder.append("\r\n");
+ }
+
+ /**
+ * inject a test string into the result parser.
+ *
+ * @param result
+ */
+ private void injectTestString(String result) {
+ byte[] data = result.getBytes();
+ mParser.addOutput(data, 0, data.length);
+ mParser.flush();
+ }
+
+ private void assertCommonAttributes() {
+ assertEquals(CLASS_NAME, mTestResult.mSuiteName);
+ assertEquals(1, mTestResult.mTestCount);
+ assertEquals(TEST_NAME, mTestResult.mTestName);
+ }
+
+ /**
+ * A specialized test listener that stores a single test events.
+ */
+ private class VerifyingTestResult implements ITestRunListener {
+
+ String mSuiteName;
+ int mTestCount;
+ int mNumTestsRun;
+ String mTestName;
+ long mTestTime;
+ TestFailure mTestStatus;
+ String mTrace;
+ boolean mStopped;
+
+ VerifyingTestResult() {
+ mNumTestsRun = 0;
+ mTestStatus = null;
+ mStopped = false;
+ }
+
+ public void testEnded(TestIdentifier test) {
+ mNumTestsRun++;
+ assertEquals("Unexpected class name", mSuiteName, test.getClassName());
+ assertEquals("Unexpected test ended", mTestName, test.getTestName());
+
+ }
+
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ mTestStatus = status;
+ mTrace = trace;
+ assertEquals("Unexpected class name", mSuiteName, test.getClassName());
+ assertEquals("Unexpected test ended", mTestName, test.getTestName());
+ }
+
+ public void testRunEnded(long elapsedTime) {
+ mTestTime = elapsedTime;
+
+ }
+
+ public void testRunStarted(int testCount) {
+ mTestCount = testCount;
+ }
+
+ public void testRunStopped(long elapsedTime) {
+ mTestTime = elapsedTime;
+ mStopped = true;
+ }
+
+ public void testStarted(TestIdentifier test) {
+ mSuiteName = test.getClassName();
+ mTestName = test.getTestName();
+ }
+
+ public void testRunFailed(String errorMessage) {
+ // ignored
+ }
+ }
+
+}
diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
new file mode 100644
index 0000000..9acaaf9
--- /dev/null
+++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.testrunner;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.Device.DeviceState;
+import com.android.ddmlib.log.LogReceiver;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Tests RemoteAndroidTestRunner.
+ */
+public class RemoteAndroidTestRunnerTest extends TestCase {
+
+ private RemoteAndroidTestRunner mRunner;
+ private MockDevice mMockDevice;
+
+ private static final String TEST_PACKAGE = "com.test";
+ private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner";
+
+ /**
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ mMockDevice = new MockDevice();
+ mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice);
+ }
+
+ /**
+ * Test the basic case building of the instrumentation runner command with no arguments.
+ */
+ public void testRun() {
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER),
+ mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with log set.
+ */
+ public void testRunWithLog() {
+ mRunner.setLogOnly(true);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE,
+ TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with method set.
+ */
+ public void testRunWithMethod() {
+ final String className = "FooTest";
+ final String testName = "fooTest";
+ mRunner.setMethodName(className, testName);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className,
+ testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+ /**
+ * Test the building of the instrumentation runner command with extra args set.
+ */
+ public void testRunWithExtraArgs() {
+ final String extraArgs = "blah";
+ mRunner.setExtraArgs(extraArgs);
+ mRunner.run(new EmptyListener());
+ assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs,
+ TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+ }
+
+
+ /**
+ * Assert two strings are equal ignoring whitespace.
+ */
+ private void assertStringsEquals(String str1, String str2) {
+ String strippedStr1 = str1.replaceAll(" ", "");
+ String strippedStr2 = str2.replaceAll(" ", "");
+ assertEquals(strippedStr1, strippedStr2);
+ }
+
+ /**
+ * A dummy device that does nothing except store the provided executed shell command for
+ * later retrieval.
+ */
+ private static class MockDevice implements IDevice {
+
+ private String mLastShellCommand;
+
+ /**
+ * Stores the provided command for later retrieval from getLastShellCommand.
+ */
+ public void executeShellCommand(String command,
+ IShellOutputReceiver receiver) throws IOException {
+ mLastShellCommand = command;
+ }
+
+ /**
+ * Get the last command provided to executeShellCommand.
+ */
+ public String getLastShellCommand() {
+ return mLastShellCommand;
+ }
+
+ public boolean createForward(int localPort, int remotePort) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Client getClient(String applicationName) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getClientName(int pid) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Client[] getClients() {
+ throw new UnsupportedOperationException();
+ }
+
+ public FileListingService getFileListingService() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Map<String, String> getProperties() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getProperty(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getPropertyCount() {
+ throw new UnsupportedOperationException();
+ }
+
+ public RawImage getScreenshot() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getSerialNumber() {
+ throw new UnsupportedOperationException();
+ }
+
+ public DeviceState getState() {
+ throw new UnsupportedOperationException();
+ }
+
+ public SyncService getSyncService() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean hasClients() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isBootLoader() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isEmulator() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isOffline() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isOnline() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean removeForward(int localPort, int remotePort) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void runEventLogService(LogReceiver receiver) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public void runLogService(String logname, LogReceiver receiver) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getAvdName() {
+ return "";
+ }
+
+ }
+
+ /**
+ * An empty implementation of ITestRunListener.
+ */
+ private static class EmptyListener implements ITestRunListener {
+
+ public void testEnded(TestIdentifier test) {
+ // ignore
+ }
+
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ // ignore
+ }
+
+ public void testRunEnded(long elapsedTime) {
+ // ignore
+ }
+
+ public void testRunFailed(String errorMessage) {
+ // ignore
+ }
+
+ public void testRunStarted(int testCount) {
+ // ignore
+ }
+
+ public void testRunStopped(long elapsedTime) {
+ // ignore
+ }
+
+ public void testStarted(TestIdentifier test) {
+ // ignore
+ }
+
+ }
+}