/*
 * 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.annotations.NonNull;
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;
import java.util.concurrent.TimeUnit;

/**
 * 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 status change */
    public static final int CHANGE_DEBUGGER_STATUS            = 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: allocation information updated */
    public static final int CHANGE_METHOD_PROFILING_STATUS    = 0x0800;

    /** Client change bit mask: combination of {@link Client#CHANGE_NAME},
     * {@link Client#CHANGE_DEBUGGER_STATUS}, and {@link Client#CHANGE_PORT}.
     */
    public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_STATUS | 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 = 800*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 IDevice} on which this Client is running.
     */
    public IDevice getDevice() {
        return mDevice;
    }

    /** Returns the {@link Device} on which this Client is running.
     */
    Device getDeviceImpl() {
        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.
     */
    @NonNull
    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
        }
    }

    /**
     * Makes the VM dump an HPROF file
     */
    public void dumpHprof() {
        boolean canStream = mClientData.hasFeature(ClientData.FEATURE_HPROF_STREAMING);
        try {
            if (canStream) {
                HandleHeap.sendHPDS(this);
            } else {
                String file = "/sdcard/" + mClientData.getClientDescription().replaceAll(
                        "\\:.*", "") + ".hprof";
                HandleHeap.sendHPDU(this, file);
            }
        } catch (IOException e) {
            Log.w("ddms", "Send of HPDU message failed");
            // ignore
        }
    }

    /**
     * Toggles method profiling state.
     * @deprecated Use {@link #startMethodTracer()}, {@link #stopMethodTracer()},
     * {@link #startSamplingProfiler(int, java.util.concurrent.TimeUnit)} or
     * {@link #stopSamplingProfiler()} instead.
     */
    public void toggleMethodProfiling() {
        try {
            switch (mClientData.getMethodProfilingStatus()) {
                case TRACER_ON:
                    stopMethodTracer();
                    break;
                case SAMPLER_ON:
                    stopSamplingProfiler();
                    break;
                case OFF:
                    startMethodTracer();
                    break;
            }
        } catch (IOException e) {
            Log.w("ddms", "Toggle method profiling failed");
            // ignore
        }
    }

    private int getProfileBufferSize() {
        return DdmPreferences.getProfilerBufferSizeMb() * 1024 * 1024;
    }

    public void startMethodTracer() throws IOException {
        boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING);
        int bufferSize = getProfileBufferSize();
        if (canStream) {
            HandleProfiling.sendMPSS(this, bufferSize, 0 /*flags*/);
        } else {
            String file = "/sdcard/" +
                    mClientData.getClientDescription().replaceAll("\\:.*", "") +
                    DdmConstants.DOT_TRACE;
            HandleProfiling.sendMPRS(this, file, bufferSize, 0 /*flags*/);
        }
    }

    public void stopMethodTracer() throws IOException {
        boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING);

        if (canStream) {
            HandleProfiling.sendMPSE(this);
        } else {
            HandleProfiling.sendMPRE(this);
        }
    }

    public void startSamplingProfiler(int samplingInterval, TimeUnit timeUnit) throws IOException {
        int bufferSize = getProfileBufferSize();
        HandleProfiling.sendSPSS(this, bufferSize, samplingInterval, timeUnit);
    }

    public void stopSamplingProfiler() throws IOException {
        HandleProfiling.sendSPSE(this);
    }

    public boolean startOpenGlTracing() {
        boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING);
        if (!canTraceOpenGl) {
            return false;
        }

        try {
            HandleViewDebug.sendStartGlTracing(this);
            return true;
        } catch (IOException e) {
            Log.w("ddms", "Start OpenGL Tracing failed");
            return false;
        }
    }

    public boolean stopOpenGlTracing() {
        boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING);
        if (!canTraceOpenGl) {
            return false;
        }

        try {
            HandleViewDebug.sendStopGlTracing(this);
            return true;
        } catch (IOException e) {
            Log.w("ddms", "Stop OpenGL Tracing failed");
            return false;
        }
    }

    /**
     * Sends a request to the VM to send the enable status of the method profiling.
     * 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 requestMethodProfilingStatus() {
        try {
            HandleHeap.sendREAQ(this);
        } catch (IOException e) {
            Log.e("ddmlib", e);
        }
    }


    /**
     * 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) {
            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 information. 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.d("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.d("ddms",
                        "Good handshake from client, sending HELO to " + mClientData.getPid());
                    JdwpPacket.consumeHandshake(mReadBuffer);
                    mConnState = ST_NEED_DDM_PKT;
                    HandleHello.sendHelloCommands(this, SERVER_PROTOCOL_VERSION);
                    // see if we have another packet in the buffer
                    return getJdwpPacket();
                case JdwpPacket.HANDSHAKE_BAD:
                    Log.d("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.d("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.d("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.d("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);
    }
}

