Add test for streaming display contents to an accessory.

There are two applications: a source and a sink.
They should be installed on two separate Android devices.
Then connect the source device to the sink device using
a USB OTG cable.

Bug: 9192512
Change-Id: I99b552026684abbfd69cb13ab324e72fa16c36ab
diff --git a/tests/AccessoryDisplay/Android.mk b/tests/AccessoryDisplay/Android.mk
new file mode 100644
index 0000000..85cb309
--- /dev/null
+++ b/tests/AccessoryDisplay/Android.mk
@@ -0,0 +1,17 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/AccessoryDisplay/README b/tests/AccessoryDisplay/README
new file mode 100644
index 0000000..5ce558c
--- /dev/null
+++ b/tests/AccessoryDisplay/README
@@ -0,0 +1,50 @@
+This directory contains sample code to test the use of virtual
+displays created over an Android Open Accessories Protocol link.
+
+--- DESCRIPTION ---
+
+There are two applications with two distinct roles: a sink
+and a source.
+
+1. Sink Application
+
+The role of the sink is to emulate an external display that happens
+to be connected using the USB accessory protocol.  Think of it as
+a monitor or video dock that the user will want to plug a phone into.
+
+The sink application uses the UsbDevice APIs to receive connections
+from the source device over USB.  The sink acts as a USB host
+in this arrangement and will provide power to the source.
+
+The sink application decodes encoded video from the source and
+displays it in a SurfaceView.  The sink also injects passes touch
+events to the source over USB HID.
+
+2. Source Application
+
+The role of the source is to present some content onto an external
+display that happens to be attached over USB.  This is the typical
+role that a phone or tablet might have when the user is trying to
+play content to an external monitor.
+
+The source application uses the UsbAccessory APIs to connect
+to the sink device over USB.  The source acts as a USB peripheral
+in this arrangement and will receive power from the sink.
+
+The source application uses the DisplayManager APIs to create
+a private virtual display which passes the framebuffer through
+an encoder and streams the output to the sink over USB.  Then
+the application opens a Presentation on the new virtual display
+and shows a silly cube animation.
+
+--- USAGE ---
+
+These applications should be installed on two separate Android
+devices which are then connected using a USB OTG cable.
+Remember that the sink device is functioning as the USB host
+so the USB OTG cable should be plugged directly into it.
+
+When connected, the applications should automatically launch
+on each device.  The source will then begin to project display
+contents to the sink.
+
diff --git a/tests/AccessoryDisplay/common/Android.mk b/tests/AccessoryDisplay/common/Android.mk
new file mode 100644
index 0000000..2d4de15
--- /dev/null
+++ b/tests/AccessoryDisplay/common/Android.mk
@@ -0,0 +1,23 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the application.
+include $(CLEAR_VARS)
+LOCAL_MODULE := AccessoryDisplayCommon
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java
new file mode 100644
index 0000000..a6bb5c1
--- /dev/null
+++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.common;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Maintains a bounded pool of buffers.  Attempts to acquire buffers beyond the maximum
+ * count will block until other buffers are released.
+ */
+final class BufferPool {
+    private final int mInitialBufferSize;
+    private final int mMaxBufferSize;
+    private final ByteBuffer[] mBuffers;
+    private int mAllocated;
+    private int mAvailable;
+
+    public BufferPool(int initialBufferSize, int maxBufferSize, int maxBuffers) {
+        mInitialBufferSize = initialBufferSize;
+        mMaxBufferSize = maxBufferSize;
+        mBuffers = new ByteBuffer[maxBuffers];
+    }
+
+    public ByteBuffer acquire(int needed) {
+        synchronized (this) {
+            for (;;) {
+                if (mAvailable != 0) {
+                    mAvailable -= 1;
+                    return grow(mBuffers[mAvailable], needed);
+                }
+
+                if (mAllocated < mBuffers.length) {
+                    mAllocated += 1;
+                    return ByteBuffer.allocate(chooseCapacity(mInitialBufferSize, needed));
+                }
+
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+        }
+    }
+
+    public void release(ByteBuffer buffer) {
+        synchronized (this) {
+            buffer.clear();
+            mBuffers[mAvailable++] = buffer;
+            notifyAll();
+        }
+    }
+
+    public ByteBuffer grow(ByteBuffer buffer, int needed) {
+        int capacity = buffer.capacity();
+        if (capacity < needed) {
+            final ByteBuffer oldBuffer = buffer;
+            capacity = chooseCapacity(capacity, needed);
+            buffer = ByteBuffer.allocate(capacity);
+            oldBuffer.flip();
+            buffer.put(oldBuffer);
+        }
+        return buffer;
+    }
+
+    private int chooseCapacity(int capacity, int needed) {
+        while (capacity < needed) {
+            capacity *= 2;
+        }
+        if (capacity > mMaxBufferSize) {
+            if (needed > mMaxBufferSize) {
+                throw new IllegalArgumentException("Requested size " + needed
+                        + " is larger than maximum buffer size " + mMaxBufferSize + ".");
+            }
+            capacity = mMaxBufferSize;
+        }
+        return capacity;
+    }
+}
diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java
new file mode 100644
index 0000000..e0b7e82
--- /dev/null
+++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.common;
+
+public abstract class Logger {
+    public abstract void log(String message);
+
+    public void logError(String message) {
+        log("ERROR: " + message);
+    }
+}
diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java
new file mode 100644
index 0000000..46fee32
--- /dev/null
+++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.common;
+
+/**
+ * Defines message types.
+ */
+public class Protocol {
+    // Message header.
+    //   0: service id (16 bits)
+    //   2: what (16 bits)
+    //   4: content size (32 bits)
+    //   8: ... content follows ...
+    static final int HEADER_SIZE = 8;
+
+    // Maximum size of a message envelope including the header and contents.
+    static final int MAX_ENVELOPE_SIZE = 64 * 1024;
+
+    /**
+     * Maximum message content size.
+     */
+    public static final int MAX_CONTENT_SIZE = MAX_ENVELOPE_SIZE - HEADER_SIZE;
+
+    public static final class DisplaySinkService {
+        private DisplaySinkService() { }
+
+        public static final int ID = 1;
+
+        // Query sink capabilities.
+        // Replies with sink available or not available.
+        public static final int MSG_QUERY = 1;
+
+        // Send MPEG2-TS H.264 encoded content.
+        public static final int MSG_CONTENT = 2;
+    }
+
+    public static final class DisplaySourceService {
+        private DisplaySourceService() { }
+
+        public static final int ID = 2;
+
+        // Sink is now available for use.
+        //   0: width (32 bits)
+        //   4: height (32 bits)
+        //   8: density dpi (32 bits)
+        public static final int MSG_SINK_AVAILABLE = 1;
+
+        // Sink is no longer available for use.
+        public static final int MSG_SINK_NOT_AVAILABLE = 2;
+    }
+}
diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java
new file mode 100644
index 0000000..70b3806
--- /dev/null
+++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.common;
+
+import com.android.accessorydisplay.common.Transport;
+
+import android.content.Context;
+import android.os.Looper;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base implementation of a service that communicates over a transport.
+ * <p>
+ * This object's interface is single-threaded.  It is only intended to be
+ * accessed from the {@link Looper} thread on which the transport was created.
+ * </p>
+ */
+public abstract class Service implements Transport.Callback {
+    private final Context mContext;
+    private final Transport mTransport;
+    private final int mServiceId;
+
+    public Service(Context context, Transport transport, int serviceId) {
+        mContext = context;
+        mTransport = transport;
+        mServiceId = serviceId;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public int getServiceId() {
+        return mServiceId;
+    }
+
+    public Transport getTransport() {
+        return mTransport;
+    }
+
+    public Logger getLogger() {
+        return mTransport.getLogger();
+    }
+
+    public void start() {
+        mTransport.registerService(mServiceId, this);
+    }
+
+    public void stop() {
+        mTransport.unregisterService(mServiceId);
+    }
+
+    @Override
+    public void onMessageReceived(int service, int what, ByteBuffer content) {
+    }
+}
diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java
new file mode 100644
index 0000000..84897d3
--- /dev/null
+++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.common;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * A simple message transport.
+ * <p>
+ * This object's interface is thread-safe, however incoming messages
+ * are always delivered on the {@link Looper} thread on which the transport
+ * was created.
+ * </p>
+ */
+public abstract class Transport {
+    private static final int MAX_INPUT_BUFFERS = 8;
+
+    private final Logger mLogger;
+
+    // The transport thread looper and handler.
+    private final TransportHandler mHandler;
+
+    // Lock to guard all mutable state.
+    private final Object mLock = new Object();
+
+    // The output buffer.  Set to null when the transport is closed.
+    private ByteBuffer mOutputBuffer;
+
+    // The input buffer pool.
+    private BufferPool mInputBufferPool;
+
+    // The reader thread.  Initialized when reading starts.
+    private ReaderThread mThread;
+
+    // The list of callbacks indexed by service id.
+    private final SparseArray<Callback> mServices = new SparseArray<Callback>();
+
+    public Transport(Logger logger, int maxPacketSize) {
+        mLogger = logger;
+        mHandler = new TransportHandler();
+        mOutputBuffer = ByteBuffer.allocate(maxPacketSize);
+        mInputBufferPool = new BufferPool(
+                maxPacketSize, Protocol.MAX_ENVELOPE_SIZE, MAX_INPUT_BUFFERS);
+    }
+
+    /**
+     * Gets the logger for debugging.
+     */
+    public Logger getLogger() {
+        return mLogger;
+    }
+
+    /**
+     * Gets the handler on the transport's thread.
+     */
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Closes the transport.
+     */
+    public void close() {
+        synchronized (mLock) {
+            if (mOutputBuffer != null) {
+                if (mThread == null) {
+                    ioClose();
+                } else {
+                    // If the thread was started then it will be responsible for
+                    // closing the stream when it quits because it may currently
+                    // be in the process of reading from the stream so we can't simply
+                    // shut it down right now.
+                    mThread.quit();
+                }
+                mOutputBuffer = null;
+            }
+        }
+    }
+
+    /**
+     * Sends a message.
+     *
+     * @param service The service to whom the message is addressed.
+     * @param what The message type.
+     * @param content The content, or null if there is none.
+     * @return True if the message was sent successfully, false if an error occurred.
+     */
+    public boolean sendMessage(int service, int what, ByteBuffer content) {
+        checkServiceId(service);
+        checkMessageId(what);
+
+        try {
+            synchronized (mLock) {
+                if (mOutputBuffer == null) {
+                    mLogger.logError("Send message failed because transport was closed.");
+                    return false;
+                }
+
+                final byte[] outputArray = mOutputBuffer.array();
+                final int capacity = mOutputBuffer.capacity();
+                mOutputBuffer.clear();
+                mOutputBuffer.putShort((short)service);
+                mOutputBuffer.putShort((short)what);
+                if (content == null) {
+                    mOutputBuffer.putInt(0);
+                } else {
+                    final int contentLimit = content.limit();
+                    int contentPosition = content.position();
+                    int contentRemaining = contentLimit - contentPosition;
+                    if (contentRemaining > Protocol.MAX_CONTENT_SIZE) {
+                        throw new IllegalArgumentException("Message content too large: "
+                                + contentRemaining + " > " + Protocol.MAX_CONTENT_SIZE);
+                    }
+                    mOutputBuffer.putInt(contentRemaining);
+                    while (contentRemaining != 0) {
+                        final int outputAvailable = capacity - mOutputBuffer.position();
+                        if (contentRemaining <= outputAvailable) {
+                            mOutputBuffer.put(content);
+                            break;
+                        }
+                        content.limit(contentPosition + outputAvailable);
+                        mOutputBuffer.put(content);
+                        content.limit(contentLimit);
+                        ioWrite(outputArray, 0, capacity);
+                        contentPosition += outputAvailable;
+                        contentRemaining -= outputAvailable;
+                        mOutputBuffer.clear();
+                    }
+                }
+                ioWrite(outputArray, 0, mOutputBuffer.position());
+                return true;
+            }
+        } catch (IOException ex) {
+            mLogger.logError("Send message failed: " + ex);
+            return false;
+        }
+    }
+
+    /**
+     * Starts reading messages on a separate thread.
+     */
+    public void startReading() {
+        synchronized (mLock) {
+            if (mOutputBuffer == null) {
+                throw new IllegalStateException("Transport has been closed");
+            }
+
+            mThread = new ReaderThread();
+            mThread.start();
+        }
+    }
+
+    /**
+     * Registers a service and provides a callback to receive messages.
+     *
+     * @param service The service id.
+     * @param callback The callback to use.
+     */
+    public void registerService(int service, Callback callback) {
+        checkServiceId(service);
+        if (callback == null) {
+            throw new IllegalArgumentException("callback must not be null");
+        }
+
+        synchronized (mLock) {
+            mServices.put(service, callback);
+        }
+    }
+
+    /**
+     * Unregisters a service.
+     *
+     * @param service The service to unregister.
+     */
+    public void unregisterService(int service) {
+        checkServiceId(service);
+
+        synchronized (mLock) {
+            mServices.remove(service);
+        }
+    }
+
+    private void dispatchMessageReceived(int service, int what, ByteBuffer content) {
+        final Callback callback;
+        synchronized (mLock) {
+            callback = mServices.get(service);
+        }
+        if (callback != null) {
+            callback.onMessageReceived(service, what, content);
+        } else {
+            mLogger.log("Discarding message " + what
+                    + " for unregistered service " + service);
+        }
+    }
+
+    private static void checkServiceId(int service) {
+        if (service < 0 || service > 0xffff) {
+            throw new IllegalArgumentException("service id out of range: " + service);
+        }
+    }
+
+    private static void checkMessageId(int what) {
+        if (what < 0 || what > 0xffff) {
+            throw new IllegalArgumentException("message id out of range: " + what);
+        }
+    }
+
+    // The IO methods must be safe to call on any thread.
+    // They may be called concurrently.
+    protected abstract void ioClose();
+    protected abstract int ioRead(byte[] buffer, int offset, int count)
+            throws IOException;
+    protected abstract void ioWrite(byte[] buffer, int offset, int count)
+            throws IOException;
+
+    /**
+     * Callback for services that handle received messages.
+     */
+    public interface Callback {
+        /**
+         * Indicates that a message was received.
+         *
+         * @param service The service to whom the message is addressed.
+         * @param what The message type.
+         * @param content The content, or null if there is none.
+         */
+        public void onMessageReceived(int service, int what, ByteBuffer content);
+    }
+
+    final class TransportHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            final ByteBuffer buffer = (ByteBuffer)msg.obj;
+            try {
+                final int limit = buffer.limit();
+                while (buffer.position() < limit) {
+                    final int service = buffer.getShort() & 0xffff;
+                    final int what = buffer.getShort() & 0xffff;
+                    final int contentSize = buffer.getInt();
+                    if (contentSize == 0) {
+                        dispatchMessageReceived(service, what, null);
+                    } else {
+                        final int end = buffer.position() + contentSize;
+                        buffer.limit(end);
+                        dispatchMessageReceived(service, what, buffer);
+                        buffer.limit(limit);
+                        buffer.position(end);
+                    }
+                }
+            } finally {
+                mInputBufferPool.release(buffer);
+            }
+        }
+    }
+
+    final class ReaderThread extends Thread {
+        // Set to true when quitting.
+        private volatile boolean mQuitting;
+
+        public ReaderThread() {
+            super("Accessory Display Transport");
+        }
+
+        @Override
+        public void run() {
+            loop();
+            ioClose();
+        }
+
+        private void loop() {
+            ByteBuffer buffer = null;
+            int length = Protocol.HEADER_SIZE;
+            int contentSize = -1;
+            outer: while (!mQuitting) {
+                // Get a buffer.
+                if (buffer == null) {
+                    buffer = mInputBufferPool.acquire(length);
+                } else {
+                    buffer = mInputBufferPool.grow(buffer, length);
+                }
+
+                // Read more data until needed number of bytes obtained.
+                int position = buffer.position();
+                int count;
+                try {
+                    count = ioRead(buffer.array(), position, buffer.capacity() - position);
+                    if (count < 0) {
+                        break; // end of stream
+                    }
+                } catch (IOException ex) {
+                    mLogger.logError("Read failed: " + ex);
+                    break; // error
+                }
+                position += count;
+                buffer.position(position);
+                if (contentSize < 0 && position >= Protocol.HEADER_SIZE) {
+                    contentSize = buffer.getInt(4);
+                    if (contentSize < 0 || contentSize > Protocol.MAX_CONTENT_SIZE) {
+                        mLogger.logError("Encountered invalid content size: " + contentSize);
+                        break; // malformed stream
+                    }
+                    length += contentSize;
+                }
+                if (position < length) {
+                    continue; // need more data
+                }
+
+                // There is at least one complete message in the buffer.
+                // Find the end of a contiguous chunk of complete messages.
+                int next = length;
+                int remaining;
+                for (;;) {
+                    length = Protocol.HEADER_SIZE;
+                    remaining = position - next;
+                    if (remaining < length) {
+                        contentSize = -1;
+                        break; // incomplete header, need more data
+                    }
+                    contentSize = buffer.getInt(next + 4);
+                    if (contentSize < 0 || contentSize > Protocol.MAX_CONTENT_SIZE) {
+                        mLogger.logError("Encountered invalid content size: " + contentSize);
+                        break outer; // malformed stream
+                    }
+                    length += contentSize;
+                    if (remaining < length) {
+                        break; // incomplete content, need more data
+                    }
+                    next += length;
+                }
+
+                // Post the buffer then don't modify it anymore.
+                // Now this is kind of sneaky.  We know that no other threads will
+                // be acquiring buffers from the buffer pool so we can keep on
+                // referring to this buffer as long as we don't modify its contents.
+                // This allows us to operate in a single-buffered mode if desired.
+                buffer.limit(next);
+                buffer.rewind();
+                mHandler.obtainMessage(0, buffer).sendToTarget();
+
+                // If there is an incomplete message at the end, then we will need
+                // to copy it to a fresh buffer before continuing.  In the single-buffered
+                // case, we may acquire the same buffer as before which is fine.
+                if (remaining == 0) {
+                    buffer = null;
+                } else {
+                    final ByteBuffer oldBuffer = buffer;
+                    buffer = mInputBufferPool.acquire(length);
+                    System.arraycopy(oldBuffer.array(), next, buffer.array(), 0, remaining);
+                    buffer.position(remaining);
+                }
+            }
+
+            if (buffer != null) {
+                mInputBufferPool.release(buffer);
+            }
+        }
+
+        public void quit() {
+            mQuitting = true;
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/sink/Android.mk b/tests/AccessoryDisplay/sink/Android.mk
new file mode 100644
index 0000000..772ce0c
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/Android.mk
@@ -0,0 +1,25 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the application.
+include $(CLEAR_VARS)
+LOCAL_PACKAGE_NAME := AccessoryDisplaySink
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res
+LOCAL_STATIC_JAVA_LIBRARIES := AccessoryDisplayCommon
+include $(BUILD_PACKAGE)
diff --git a/tests/AccessoryDisplay/sink/AndroidManifest.xml b/tests/AccessoryDisplay/sink/AndroidManifest.xml
new file mode 100644
index 0000000..72d498f
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.accessorydisplay.sink" >
+
+    <uses-feature android:name="android.hardware.usb.host"/>
+    <uses-sdk android:minSdkVersion="18" />
+
+    <application android:label="@string/app_name"
+            android:icon="@drawable/ic_app"
+            android:hardwareAccelerated="true">
+
+        <activity android:name=".SinkActivity"
+                android:label="@string/app_name"
+                android:theme="@android:style/Theme.Holo">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+                    android:resource="@xml/usb_device_filter"/>
+        </activity>
+
+    </application>
+</manifest>
diff --git a/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png b/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png
new file mode 100755
index 0000000..66a1984
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png
Binary files differ
diff --git a/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png b/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png
new file mode 100644
index 0000000..5ae7701
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png
Binary files differ
diff --git a/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml b/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml
new file mode 100644
index 0000000..6afb850
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+    <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:orientation="vertical">
+        <TextView android:id="@+id/fpsTextView"
+                android:layout_width="match_parent"
+                android:layout_height="64dp"
+                android:padding="4dp" />
+
+        <TextView android:id="@+id/logTextView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+    </LinearLayout>
+
+    <FrameLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="2">
+        <SurfaceView android:id="@+id/surfaceView"
+                android:layout_width="640px"
+                android:layout_height="480px" />
+    </FrameLayout>
+</LinearLayout>
diff --git a/tests/AccessoryDisplay/sink/res/values/strings.xml b/tests/AccessoryDisplay/sink/res/values/strings.xml
new file mode 100644
index 0000000..29cd001
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">Accessory Display Sink</string>
+</resources>
diff --git a/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml b/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml
new file mode 100644
index 0000000..e8fe929
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!-- Match all devices -->
+    <usb-device />
+
+    <!-- Android USB accessory: accessory -->
+    <usb-device vendor-id="16601" product-id="11520" />
+    <!-- Android USB accessory: accessory + adb -->
+    <usb-device vendor-id="16601" product-id="11521" />
+</resources>
diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java
new file mode 100644
index 0000000..daec845
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.sink;
+
+import com.android.accessorydisplay.common.Protocol;
+import com.android.accessorydisplay.common.Service;
+import com.android.accessorydisplay.common.Transport;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import java.nio.ByteBuffer;
+
+public class DisplaySinkService extends Service implements SurfaceHolder.Callback {
+    private final ByteBuffer mBuffer = ByteBuffer.allocate(12);
+    private final Handler mTransportHandler;
+    private final int mDensityDpi;
+
+    private SurfaceView mSurfaceView;
+
+    // These fields are guarded by the following lock.
+    // This is to ensure that the surface lifecycle is respected.  Although decoding
+    // happens on the transport thread, we are not allowed to access the surface after
+    // it is destroyed by the UI thread so we need to stop the codec immediately.
+    private final Object mSurfaceAndCodecLock = new Object();
+    private Surface mSurface;
+    private int mSurfaceWidth;
+    private int mSurfaceHeight;
+    private MediaCodec mCodec;
+    private ByteBuffer[] mCodecInputBuffers;
+    private BufferInfo mCodecBufferInfo;
+
+    public DisplaySinkService(Context context, Transport transport, int densityDpi) {
+        super(context, transport, Protocol.DisplaySinkService.ID);
+        mTransportHandler = transport.getHandler();
+        mDensityDpi = densityDpi;
+    }
+
+    public void setSurfaceView(final SurfaceView surfaceView) {
+        if (mSurfaceView != surfaceView) {
+            final SurfaceView oldSurfaceView = mSurfaceView;
+            mSurfaceView = surfaceView;
+
+            if (oldSurfaceView != null) {
+                oldSurfaceView.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final SurfaceHolder holder = oldSurfaceView.getHolder();
+                        holder.removeCallback(DisplaySinkService.this);
+                        updateSurfaceFromUi(null);
+                    }
+                });
+            }
+
+            if (surfaceView != null) {
+                surfaceView.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final SurfaceHolder holder = surfaceView.getHolder();
+                        holder.addCallback(DisplaySinkService.this);
+                        updateSurfaceFromUi(holder);
+                    }
+                });
+            }
+        }
+    }
+
+    @Override
+    public void onMessageReceived(int service, int what, ByteBuffer content) {
+        switch (what) {
+            case Protocol.DisplaySinkService.MSG_QUERY: {
+                getLogger().log("Received MSG_QUERY.");
+                sendSinkStatus();
+                break;
+            }
+
+            case Protocol.DisplaySinkService.MSG_CONTENT: {
+                decode(content);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        // Ignore.  Wait for surface changed event that follows.
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        updateSurfaceFromUi(holder);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        updateSurfaceFromUi(null);
+    }
+
+    private void updateSurfaceFromUi(SurfaceHolder holder) {
+        Surface surface = null;
+        int width = 0, height = 0;
+        if (holder != null && !holder.isCreating()) {
+            surface = holder.getSurface();
+            if (surface.isValid()) {
+                final Rect frame = holder.getSurfaceFrame();
+                width = frame.width();
+                height = frame.height();
+            } else {
+                surface = null;
+            }
+        }
+
+        synchronized (mSurfaceAndCodecLock) {
+            if (mSurface == surface &&  mSurfaceWidth == width && mSurfaceHeight == height) {
+                return;
+            }
+
+            mSurface = surface;
+            mSurfaceWidth = width;
+            mSurfaceHeight = height;
+
+            if (mCodec != null) {
+                mCodec.stop();
+                mCodec = null;
+                mCodecInputBuffers = null;
+                mCodecBufferInfo = null;
+            }
+
+            if (mSurface != null) {
+                MediaFormat format = MediaFormat.createVideoFormat(
+                        "video/avc", mSurfaceWidth, mSurfaceHeight);
+                mCodec = MediaCodec.createDecoderByType("video/avc");
+                mCodec.configure(format, mSurface, null, 0);
+                mCodec.start();
+                mCodecBufferInfo = new BufferInfo();
+            }
+
+            mTransportHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    sendSinkStatus();
+                }
+            });
+        }
+    }
+
+    private void decode(ByteBuffer content) {
+        if (content == null) {
+            return;
+        }
+        synchronized (mSurfaceAndCodecLock) {
+            if (mCodec == null) {
+                return;
+            }
+
+            while (content.hasRemaining()) {
+                if (!provideCodecInputLocked(content)) {
+                    getLogger().log("Dropping content because there are no available buffers.");
+                    return;
+                }
+
+                consumeCodecOutputLocked();
+            }
+        }
+    }
+
+    private boolean provideCodecInputLocked(ByteBuffer content) {
+        final int index = mCodec.dequeueInputBuffer(0);
+        if (index < 0) {
+            return false;
+        }
+        if (mCodecInputBuffers == null) {
+            mCodecInputBuffers = mCodec.getInputBuffers();
+        }
+        final ByteBuffer buffer = mCodecInputBuffers[index];
+        final int capacity = buffer.capacity();
+        buffer.clear();
+        if (content.remaining() <= capacity) {
+            buffer.put(content);
+        } else {
+            final int limit = content.limit();
+            content.limit(content.position() + capacity);
+            buffer.put(content);
+            content.limit(limit);
+        }
+        buffer.flip();
+        mCodec.queueInputBuffer(index, 0, buffer.limit(), 0, 0);
+        return true;
+    }
+
+    private void consumeCodecOutputLocked() {
+        for (;;) {
+            final int index = mCodec.dequeueOutputBuffer(mCodecBufferInfo, 0);
+            if (index >= 0) {
+                mCodec.releaseOutputBuffer(index, true /*render*/);
+            } else if (index != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
+                    && index != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+                break;
+            }
+        }
+    }
+
+    private void sendSinkStatus() {
+        synchronized (mSurfaceAndCodecLock) {
+            if (mCodec != null) {
+                mBuffer.clear();
+                mBuffer.putInt(mSurfaceWidth);
+                mBuffer.putInt(mSurfaceHeight);
+                mBuffer.putInt(mDensityDpi);
+                mBuffer.flip();
+                getTransport().sendMessage(Protocol.DisplaySourceService.ID,
+                        Protocol.DisplaySourceService.MSG_SINK_AVAILABLE, mBuffer);
+            } else {
+                getTransport().sendMessage(Protocol.DisplaySourceService.ID,
+                        Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE, null);
+            }
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java
new file mode 100644
index 0000000..6fe2cfb
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.sink;
+
+import com.android.accessorydisplay.common.Logger;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaFormat;
+import android.os.Bundle;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.TextView;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.Map;
+
+public class SinkActivity extends Activity {
+    private static final String TAG = "SinkActivity";
+
+    private static final String ACTION_USB_DEVICE_PERMISSION =
+            "com.android.accessorydisplay.sink.ACTION_USB_DEVICE_PERMISSION";
+
+    private static final String MANUFACTURER = "Android";
+    private static final String MODEL = "Accessory Display";
+    private static final String DESCRIPTION = "Accessory Display Sink Test Application";
+    private static final String VERSION = "1.0";
+    private static final String URI = "http://www.android.com/";
+    private static final String SERIAL = "0000000012345678";
+
+    private static final int MULTITOUCH_DEVICE_ID = 0;
+    private static final int MULTITOUCH_REPORT_ID = 1;
+    private static final int MULTITOUCH_MAX_CONTACTS = 1;
+
+    private UsbManager mUsbManager;
+    private DeviceReceiver mReceiver;
+    private TextView mLogTextView;
+    private TextView mFpsTextView;
+    private SurfaceView mSurfaceView;
+    private Logger mLogger;
+
+    private boolean mConnected;
+    private int mProtocolVersion;
+    private UsbDevice mDevice;
+    private UsbInterface mAccessoryInterface;
+    private UsbDeviceConnection mAccessoryConnection;
+    private UsbEndpoint mControlEndpoint;
+    private UsbAccessoryBulkTransport mTransport;
+
+    private boolean mAttached;
+    private DisplaySinkService mDisplaySinkService;
+
+    private final ByteBuffer mHidBuffer = ByteBuffer.allocate(4096);
+    private UsbHid.Multitouch mMultitouch;
+    private boolean mMultitouchEnabled;
+    private UsbHid.Multitouch.Contact[] mMultitouchContacts;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE);
+
+        setContentView(R.layout.sink_activity);
+
+        mLogTextView = (TextView) findViewById(R.id.logTextView);
+        mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance());
+        mLogger = new TextLogger();
+
+        mFpsTextView = (TextView) findViewById(R.id.fpsTextView);
+
+        mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
+        mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                sendHidTouch(event);
+                return true;
+            }
+        });
+
+        mLogger.log("Waiting for accessory display source to be attached to USB...");
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+        filter.addAction(ACTION_USB_DEVICE_PERMISSION);
+        mReceiver = new DeviceReceiver();
+        registerReceiver(mReceiver, filter);
+
+        Intent intent = getIntent();
+        if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
+            UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
+            if (device != null) {
+                onDeviceAttached(device);
+            }
+        } else {
+            Map<String, UsbDevice> devices = mUsbManager.getDeviceList();
+            if (devices != null) {
+                for (UsbDevice device : devices.values()) {
+                    onDeviceAttached(device);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        unregisterReceiver(mReceiver);
+    }
+
+    private void onDeviceAttached(UsbDevice device) {
+        mLogger.log("USB device attached: " + device);
+        if (!mConnected) {
+            connect(device);
+        }
+    }
+
+    private void onDeviceDetached(UsbDevice device) {
+        mLogger.log("USB device detached: " + device);
+        if (mConnected && device.equals(mDevice)) {
+            disconnect();
+        }
+    }
+
+    private void connect(UsbDevice device) {
+        if (mConnected) {
+            disconnect();
+        }
+
+        // Check whether we have permission to access the device.
+        if (!mUsbManager.hasPermission(device)) {
+            mLogger.log("Prompting the user for access to the device.");
+            Intent intent = new Intent(ACTION_USB_DEVICE_PERMISSION);
+            intent.setPackage(getPackageName());
+            PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+            mUsbManager.requestPermission(device, pendingIntent);
+            return;
+        }
+
+        // Claim the device.
+        UsbDeviceConnection conn = mUsbManager.openDevice(device);
+        if (conn == null) {
+            mLogger.logError("Could not obtain device connection.");
+            return;
+        }
+        UsbInterface iface = device.getInterface(0);
+        UsbEndpoint controlEndpoint = iface.getEndpoint(0);
+        if (!conn.claimInterface(iface, true)) {
+            mLogger.logError("Could not claim interface.");
+            return;
+        }
+        try {
+            // If already in accessory mode, then connect to the device.
+            if (isAccessory(device)) {
+                mLogger.log("Connecting to accessory...");
+
+                int protocolVersion = getProtocol(conn);
+                if (protocolVersion < 1) {
+                    mLogger.logError("Device does not support accessory protocol.");
+                    return;
+                }
+                mLogger.log("Protocol version: " + protocolVersion);
+
+                // Setup bulk endpoints.
+                UsbEndpoint bulkIn = null;
+                UsbEndpoint bulkOut = null;
+                for (int i = 0; i < iface.getEndpointCount(); i++) {
+                    UsbEndpoint ep = iface.getEndpoint(i);
+                    if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
+                        if (bulkIn == null) {
+                            mLogger.log(String.format("Bulk IN endpoint: %d", i));
+                            bulkIn = ep;
+                        }
+                    } else {
+                        if (bulkOut == null) {
+                            mLogger.log(String.format("Bulk OUT endpoint: %d", i));
+                            bulkOut = ep;
+                        }
+                    }
+                }
+                if (bulkIn == null || bulkOut == null) {
+                    mLogger.logError("Unable to find bulk endpoints");
+                    return;
+                }
+
+                mLogger.log("Connected");
+                mConnected = true;
+                mDevice = device;
+                mProtocolVersion = protocolVersion;
+                mAccessoryInterface = iface;
+                mAccessoryConnection = conn;
+                mControlEndpoint = controlEndpoint;
+                mTransport = new UsbAccessoryBulkTransport(mLogger, conn, bulkIn, bulkOut);
+                if (mProtocolVersion >= 2) {
+                    registerHid();
+                }
+                startServices();
+                mTransport.startReading();
+                return;
+            }
+
+            // Do accessory negotiation.
+            mLogger.log("Attempting to switch device to accessory mode...");
+
+            // Send get protocol.
+            int protocolVersion = getProtocol(conn);
+            if (protocolVersion < 1) {
+                mLogger.logError("Device does not support accessory protocol.");
+                return;
+            }
+            mLogger.log("Protocol version: " + protocolVersion);
+
+            // Send identifying strings.
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MANUFACTURER, MANUFACTURER);
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MODEL, MODEL);
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_DESCRIPTION, DESCRIPTION);
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_VERSION, VERSION);
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_URI, URI);
+            sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_SERIAL, SERIAL);
+
+            // Send start.
+            // The device should re-enumerate as an accessory.
+            mLogger.log("Sending accessory start request.");
+            int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+                    UsbAccessoryConstants.ACCESSORY_START, 0, 0, null, 0, 10000);
+            if (len != 0) {
+                mLogger.logError("Device refused to switch to accessory mode.");
+            } else {
+                mLogger.log("Waiting for device to re-enumerate...");
+            }
+        } finally {
+            if (!mConnected) {
+                conn.releaseInterface(iface);
+            }
+        }
+    }
+
+    private void disconnect() {
+        mLogger.log("Disconnecting from device: " + mDevice);
+        stopServices();
+        unregisterHid();
+
+        mLogger.log("Disconnected.");
+        mConnected = false;
+        mDevice = null;
+        mAccessoryConnection = null;
+        mAccessoryInterface = null;
+        mControlEndpoint = null;
+        if (mTransport != null) {
+            mTransport.close();
+            mTransport = null;
+        }
+    }
+
+    private void registerHid() {
+        mLogger.log("Registering HID multitouch device.");
+
+        mMultitouch = new UsbHid.Multitouch(MULTITOUCH_REPORT_ID, MULTITOUCH_MAX_CONTACTS,
+                mSurfaceView.getWidth(), mSurfaceView.getHeight());
+
+        mHidBuffer.clear();
+        mMultitouch.generateDescriptor(mHidBuffer);
+        mHidBuffer.flip();
+
+        mLogger.log("HID descriptor size: " + mHidBuffer.limit());
+        mLogger.log("HID report size: " + mMultitouch.getReportSize());
+
+        final int maxPacketSize = mControlEndpoint.getMaxPacketSize();
+        mLogger.log("Control endpoint max packet size: " + maxPacketSize);
+        if (mMultitouch.getReportSize() > maxPacketSize) {
+            mLogger.logError("HID report is too big for this accessory.");
+            return;
+        }
+
+        int len = mAccessoryConnection.controlTransfer(
+                UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+                UsbAccessoryConstants.ACCESSORY_REGISTER_HID,
+                MULTITOUCH_DEVICE_ID, mHidBuffer.limit(), null, 0, 10000);
+        if (len != 0) {
+            mLogger.logError("Device rejected ACCESSORY_REGISTER_HID request.");
+            return;
+        }
+
+        while (mHidBuffer.hasRemaining()) {
+            int position = mHidBuffer.position();
+            int count = Math.min(mHidBuffer.remaining(), maxPacketSize);
+            len = mAccessoryConnection.controlTransfer(
+                    UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+                    UsbAccessoryConstants.ACCESSORY_SET_HID_REPORT_DESC,
+                    MULTITOUCH_DEVICE_ID, 0,
+                    mHidBuffer.array(), position, count, 10000);
+            if (len != count) {
+                mLogger.logError("Device rejected ACCESSORY_SET_HID_REPORT_DESC request.");
+                return;
+            }
+            mHidBuffer.position(position + count);
+        }
+
+        mLogger.log("HID device registered.");
+
+        mMultitouchEnabled = true;
+        if (mMultitouchContacts == null) {
+            mMultitouchContacts = new UsbHid.Multitouch.Contact[MULTITOUCH_MAX_CONTACTS];
+            for (int i = 0; i < MULTITOUCH_MAX_CONTACTS; i++) {
+                mMultitouchContacts[i] = new UsbHid.Multitouch.Contact();
+            }
+        }
+    }
+
+    private void unregisterHid() {
+        mMultitouch = null;
+        mMultitouchContacts = null;
+        mMultitouchEnabled = false;
+    }
+
+    private void sendHidTouch(MotionEvent event) {
+        if (mMultitouchEnabled) {
+            mLogger.log("Sending touch event: " + event);
+
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_MOVE: {
+                    final int pointerCount =
+                            Math.min(MULTITOUCH_MAX_CONTACTS, event.getPointerCount());
+                    final int historySize = event.getHistorySize();
+                    for (int p = 0; p < pointerCount; p++) {
+                        mMultitouchContacts[p].id = event.getPointerId(p);
+                    }
+                    for (int h = 0; h < historySize; h++) {
+                        for (int p = 0; p < pointerCount; p++) {
+                            mMultitouchContacts[p].x = (int)event.getHistoricalX(p, h);
+                            mMultitouchContacts[p].y = (int)event.getHistoricalY(p, h);
+                        }
+                        sendHidTouchReport(pointerCount);
+                    }
+                    for (int p = 0; p < pointerCount; p++) {
+                        mMultitouchContacts[p].x = (int)event.getX(p);
+                        mMultitouchContacts[p].y = (int)event.getY(p);
+                    }
+                    sendHidTouchReport(pointerCount);
+                    break;
+                }
+
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_UP:
+                    sendHidTouchReport(0);
+                    break;
+            }
+        }
+    }
+
+    private void sendHidTouchReport(int contactCount) {
+        mHidBuffer.clear();
+        mMultitouch.generateReport(mHidBuffer, mMultitouchContacts, contactCount);
+        mHidBuffer.flip();
+
+        int count = mHidBuffer.limit();
+        int len = mAccessoryConnection.controlTransfer(
+                UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+                UsbAccessoryConstants.ACCESSORY_SEND_HID_EVENT,
+                MULTITOUCH_DEVICE_ID, 0,
+                mHidBuffer.array(), 0, count, 10000);
+        if (len != count) {
+            mLogger.logError("Device rejected ACCESSORY_SEND_HID_EVENT request.");
+            return;
+        }
+    }
+
+    private void startServices() {
+        mDisplaySinkService = new DisplaySinkService(this, mTransport,
+                getResources().getConfiguration().densityDpi);
+        mDisplaySinkService.start();
+
+        if (mAttached) {
+            mDisplaySinkService.setSurfaceView(mSurfaceView);
+        }
+    }
+
+    private void stopServices() {
+        if (mDisplaySinkService != null) {
+            mDisplaySinkService.stop();
+            mDisplaySinkService = null;
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mAttached = true;
+        if (mDisplaySinkService != null) {
+            mDisplaySinkService.setSurfaceView(mSurfaceView);
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mAttached = false;
+        if (mDisplaySinkService != null) {
+            mDisplaySinkService.setSurfaceView(null);
+        }
+    }
+
+    private int getProtocol(UsbDeviceConnection conn) {
+        byte buffer[] = new byte[2];
+        int len = conn.controlTransfer(
+                UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR,
+                UsbAccessoryConstants.ACCESSORY_GET_PROTOCOL, 0, 0, buffer, 2, 10000);
+        if (len != 2) {
+            return -1;
+        }
+        return (buffer[1] << 8) | buffer[0];
+    }
+
+    private void sendString(UsbDeviceConnection conn, int index, String string) {
+        byte[] buffer = (string + "\0").getBytes();
+        int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+                UsbAccessoryConstants.ACCESSORY_SEND_STRING, 0, index,
+                buffer, buffer.length, 10000);
+        if (len != buffer.length) {
+            mLogger.logError("Failed to send string " + index + ": \"" + string + "\"");
+        } else {
+            mLogger.log("Sent string " + index + ": \"" + string + "\"");
+        }
+    }
+
+    private static boolean isAccessory(UsbDevice device) {
+        final int vid = device.getVendorId();
+        final int pid = device.getProductId();
+        return vid == UsbAccessoryConstants.USB_ACCESSORY_VENDOR_ID
+                && (pid == UsbAccessoryConstants.USB_ACCESSORY_PRODUCT_ID
+                        || pid == UsbAccessoryConstants.USB_ACCESSORY_ADB_PRODUCT_ID);
+    }
+
+    class TextLogger extends Logger {
+        @Override
+        public void log(final String message) {
+            Log.d(TAG, message);
+
+            mLogTextView.post(new Runnable() {
+                @Override
+                public void run() {
+                    mLogTextView.append(message);
+                    mLogTextView.append("\n");
+                }
+            });
+        }
+    }
+
+    class DeviceReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
+            if (device != null) {
+                String action = intent.getAction();
+                if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
+                    onDeviceAttached(device);
+                } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
+                    onDeviceDetached(device);
+                } else if (action.equals(ACTION_USB_DEVICE_PERMISSION)) {
+                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
+                        mLogger.log("Device permission granted: " + device);
+                        onDeviceAttached(device);
+                    } else {
+                        mLogger.logError("Device permission denied: " + device);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java
new file mode 100644
index 0000000..a15bfad
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.sink;
+
+import com.android.accessorydisplay.common.Logger;
+import com.android.accessorydisplay.common.Transport;
+
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+
+import java.io.IOException;
+
+/**
+ * Sends or receives messages using bulk endpoints associated with a {@link UsbDevice}
+ * that represents a USB accessory.
+ */
+public class UsbAccessoryBulkTransport extends Transport {
+    private static final int TIMEOUT_MILLIS = 1000;
+
+    private UsbDeviceConnection mConnection;
+    private UsbEndpoint mBulkInEndpoint;
+    private UsbEndpoint mBulkOutEndpoint;
+
+    public UsbAccessoryBulkTransport(Logger logger, UsbDeviceConnection connection,
+            UsbEndpoint bulkInEndpoint, UsbEndpoint bulkOutEndpoint) {
+        super(logger, 16384);
+        mConnection = connection;
+        mBulkInEndpoint = bulkInEndpoint;
+        mBulkOutEndpoint = bulkOutEndpoint;
+    }
+
+    @Override
+    protected void ioClose() {
+        mConnection = null;
+        mBulkInEndpoint = null;
+        mBulkOutEndpoint = null;
+    }
+
+    @Override
+    protected int ioRead(byte[] buffer, int offset, int count) throws IOException {
+        if (mConnection == null) {
+            throw new IOException("Connection was closed.");
+        }
+        return mConnection.bulkTransfer(mBulkInEndpoint, buffer, offset, count, -1);
+    }
+
+    @Override
+    protected void ioWrite(byte[] buffer, int offset, int count) throws IOException {
+        if (mConnection == null) {
+            throw new IOException("Connection was closed.");
+        }
+        int result = mConnection.bulkTransfer(mBulkOutEndpoint,
+                buffer, offset, count, TIMEOUT_MILLIS);
+        if (result < 0) {
+            throw new IOException("Bulk transfer failed.");
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java
new file mode 100644
index 0000000..8197d6b
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.sink;
+
+// Constants from kernel include/linux/usb/f_accessory.h
+final class UsbAccessoryConstants {
+    /* Use Google Vendor ID when in accessory mode */
+    public static final int USB_ACCESSORY_VENDOR_ID = 0x18D1;
+
+    /* Product ID to use when in accessory mode */
+    public static final int USB_ACCESSORY_PRODUCT_ID = 0x2D00;
+
+    /* Product ID to use when in accessory mode and adb is enabled */
+    public static final int USB_ACCESSORY_ADB_PRODUCT_ID = 0x2D01;
+
+    /* Indexes for strings sent by the host via ACCESSORY_SEND_STRING */
+    public static final int ACCESSORY_STRING_MANUFACTURER = 0;
+    public static final int ACCESSORY_STRING_MODEL = 1;
+    public static final int ACCESSORY_STRING_DESCRIPTION = 2;
+    public static final int ACCESSORY_STRING_VERSION = 3;
+    public static final int ACCESSORY_STRING_URI = 4;
+    public static final int ACCESSORY_STRING_SERIAL = 5;
+
+    /* Control request for retrieving device's protocol version
+     *
+     *  requestType:    USB_DIR_IN | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_GET_PROTOCOL
+     *  value:          0
+     *  index:          0
+     *  data            version number (16 bits little endian)
+     *                     1 for original accessory support
+     *                     2 adds HID and device to host audio support
+     */
+    public static final int ACCESSORY_GET_PROTOCOL = 51;
+
+    /* Control request for host to send a string to the device
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_SEND_STRING
+     *  value:          0
+     *  index:          string ID
+     *  data            zero terminated UTF8 string
+     *
+     *  The device can later retrieve these strings via the
+     *  ACCESSORY_GET_STRING_* ioctls
+     */
+    public static final int ACCESSORY_SEND_STRING = 52;
+
+    /* Control request for starting device in accessory mode.
+     * The host sends this after setting all its strings to the device.
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_START
+     *  value:          0
+     *  index:          0
+     *  data            none
+     */
+    public static final int ACCESSORY_START = 53;
+
+    /* Control request for registering a HID device.
+     * Upon registering, a unique ID is sent by the accessory in the
+     * value parameter. This ID will be used for future commands for
+     * the device
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_REGISTER_HID_DEVICE
+     *  value:          Accessory assigned ID for the HID device
+     *  index:          total length of the HID report descriptor
+     *  data            none
+     */
+    public static final int ACCESSORY_REGISTER_HID = 54;
+
+    /* Control request for unregistering a HID device.
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_REGISTER_HID
+     *  value:          Accessory assigned ID for the HID device
+     *  index:          0
+     *  data            none
+     */
+    public static final int ACCESSORY_UNREGISTER_HID = 55;
+
+    /* Control request for sending the HID report descriptor.
+     * If the HID descriptor is longer than the endpoint zero max packet size,
+     * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC
+     * commands. The data for the descriptor must be sent sequentially
+     * if multiple packets are needed.
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_SET_HID_REPORT_DESC
+     *  value:          Accessory assigned ID for the HID device
+     *  index:          offset of data in descriptor
+     *                      (needed when HID descriptor is too big for one packet)
+     *  data            the HID report descriptor
+     */
+    public static final int ACCESSORY_SET_HID_REPORT_DESC = 56;
+
+    /* Control request for sending HID events.
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_SEND_HID_EVENT
+     *  value:          Accessory assigned ID for the HID device
+     *  index:          0
+     *  data            the HID report for the event
+     */
+    public static final int ACCESSORY_SEND_HID_EVENT = 57;
+
+    /* Control request for setting the audio mode.
+     *
+     *  requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
+     *  request:        ACCESSORY_SET_AUDIO_MODE
+     *  value:          0 - no audio
+     *                     1 - device to host, 44100 16-bit stereo PCM
+     *  index:          0
+     *  data            none
+     */
+    public static final int ACCESSORY_SET_AUDIO_MODE = 58;
+
+    private UsbAccessoryConstants() {
+    }
+}
diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java
new file mode 100644
index 0000000..b4fa1fd
--- /dev/null
+++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.sink;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Helper for creating USB HID descriptors and reports.
+ */
+final class UsbHid {
+    private UsbHid() {
+    }
+
+    /**
+     * Generates basic Windows 7 compatible HID multitouch descriptors and reports
+     * that should be supported by recent versions of the Linux hid-multitouch driver.
+     */
+    public static final class Multitouch {
+        private final int mReportId;
+        private final int mMaxContacts;
+        private final int mWidth;
+        private final int mHeight;
+
+        public Multitouch(int reportId, int maxContacts, int width, int height) {
+            mReportId = reportId;
+            mMaxContacts = maxContacts;
+            mWidth = width;
+            mHeight = height;
+        }
+
+        public void generateDescriptor(ByteBuffer buffer) {
+            buffer.put(new byte[] {
+                0x05, 0x0d,                         // USAGE_PAGE (Digitizers)
+                0x09, 0x04,                         // USAGE (Touch Screen)
+                (byte)0xa1, 0x01,                   // COLLECTION (Application)
+                (byte)0x85, (byte)mReportId,        //   REPORT_ID (Touch)
+                0x09, 0x22,                         //   USAGE (Finger)
+                (byte)0xa1, 0x00,                   //   COLLECTION (Physical)
+                0x09, 0x55,                         //     USAGE (Contact Count Maximum)
+                0x15, 0x00,                         //     LOGICAL_MINIMUM (0)
+                0x25, (byte)mMaxContacts,           //     LOGICAL_MAXIMUM (...)
+                0x75, 0x08,                         //     REPORT_SIZE (8)
+                (byte)0x95, 0x01,                   //     REPORT_COUNT (1)
+                (byte)0xb1, (byte)mMaxContacts,     //     FEATURE (Data,Var,Abs)
+                0x09, 0x54,                         //     USAGE (Contact Count)
+                (byte)0x81, 0x02,                   //     INPUT (Data,Var,Abs)
+            });
+            byte maxXLsb = (byte)(mWidth - 1);
+            byte maxXMsb = (byte)((mWidth - 1) >> 8);
+            byte maxYLsb = (byte)(mHeight - 1);
+            byte maxYMsb = (byte)((mHeight - 1) >> 8);
+            byte[] collection = new byte[] { 
+                0x05, 0x0d,                         //     USAGE_PAGE (Digitizers)
+                0x09, 0x22,                         //     USAGE (Finger)
+                (byte)0xa1, 0x02,                   //     COLLECTION (Logical)
+                0x09, 0x42,                         //       USAGE (Tip Switch)
+                0x15, 0x00,                         //       LOGICAL_MINIMUM (0)
+                0x25, 0x01,                         //       LOGICAL_MAXIMUM (1)
+                0x75, 0x01,                         //       REPORT_SIZE (1)
+                (byte)0x81, 0x02,                   //       INPUT (Data,Var,Abs)
+                0x09, 0x32,                         //       USAGE (In Range)
+                (byte)0x81, 0x02,                   //       INPUT (Data,Var,Abs)
+                0x09, 0x51,                         //       USAGE (Contact Identifier)
+                0x25, 0x3f,                         //       LOGICAL_MAXIMUM (63)
+                0x75, 0x06,                         //       REPORT_SIZE (6)
+                (byte)0x81, 0x02,                   //       INPUT (Data,Var,Abs)
+                0x05, 0x01,                         //       USAGE_PAGE (Generic Desktop)
+                0x09, 0x30,                         //       USAGE (X)
+                0x26, maxXLsb, maxXMsb,             //       LOGICAL_MAXIMUM (...)
+                0x75, 0x10,                         //       REPORT_SIZE (16)
+                (byte)0x81, 0x02,                   //       INPUT (Data,Var,Abs)
+                0x09, 0x31,                         //       USAGE (Y)
+                0x26, maxYLsb, maxYMsb,             //       LOGICAL_MAXIMUM (...)
+                (byte)0x81, 0x02,                   //       INPUT (Data,Var,Abs)
+                (byte)0xc0,                         //     END_COLLECTION
+            };
+            for (int i = 0; i < mMaxContacts; i++) {
+                buffer.put(collection);
+            }
+            buffer.put(new byte[] {
+                (byte)0xc0,                         //   END_COLLECTION
+                (byte)0xc0,                         // END_COLLECTION
+            });
+        }
+
+        public void generateReport(ByteBuffer buffer, Contact[] contacts, int contactCount) {
+            // Report Id
+            buffer.put((byte)mReportId);
+            // Contact Count
+            buffer.put((byte)contactCount);
+
+            for (int i = 0; i < contactCount; i++) {
+                final Contact contact = contacts[i];
+                // Tip Switch, In Range, Contact Identifier
+                buffer.put((byte)((contact.id << 2) | 0x03));
+                // X
+                buffer.put((byte)contact.x).put((byte)(contact.x >> 8));
+                // Y
+                buffer.put((byte)contact.y).put((byte)(contact.y >> 8));
+            }
+            for (int i = contactCount; i < mMaxContacts; i++) {
+                buffer.put((byte)0).put((byte)0).put((byte)0).put((byte)0).put((byte)0);
+            }
+        }
+
+        public int getReportSize() {
+            return 2 + mMaxContacts * 5;
+        }
+
+        public static final class Contact {
+            public int id; // range 0..63
+            public int x;
+            public int y;
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/source/Android.mk b/tests/AccessoryDisplay/source/Android.mk
new file mode 100644
index 0000000..5d1085d
--- /dev/null
+++ b/tests/AccessoryDisplay/source/Android.mk
@@ -0,0 +1,25 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the application.
+include $(CLEAR_VARS)
+LOCAL_PACKAGE_NAME := AccessoryDisplaySource
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res
+LOCAL_STATIC_JAVA_LIBRARIES := AccessoryDisplayCommon
+include $(BUILD_PACKAGE)
diff --git a/tests/AccessoryDisplay/source/AndroidManifest.xml b/tests/AccessoryDisplay/source/AndroidManifest.xml
new file mode 100644
index 0000000..d3edcb8
--- /dev/null
+++ b/tests/AccessoryDisplay/source/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.accessorydisplay.source" >
+
+    <uses-feature android:name="android.hardware.usb.accessory"/>
+    <uses-sdk android:minSdkVersion="18" />
+
+    <application android:label="@string/app_name"
+            android:icon="@drawable/ic_app"
+            android:hardwareAccelerated="true">
+
+        <activity android:name=".SourceActivity"
+                android:label="@string/app_name"
+                android:theme="@android:style/Theme.Holo">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
+                    android:resource="@xml/usb_accessory_filter"/>
+        </activity>
+
+    </application>
+</manifest>
diff --git a/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png b/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png
new file mode 100755
index 0000000..66a1984
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png
Binary files differ
diff --git a/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png b/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png
new file mode 100644
index 0000000..5ae7701
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png
Binary files differ
diff --git a/tests/AccessoryDisplay/source/res/layout/presentation_content.xml b/tests/AccessoryDisplay/source/res/layout/presentation_content.xml
new file mode 100644
index 0000000..bf9566a
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/layout/presentation_content.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <android.opengl.GLSurfaceView android:id="@+id/surface_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    <Button android:id="@+id/explode_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_alignParentBottom="true"
+            android:text="Explode!" />
+</RelativeLayout>
diff --git a/tests/AccessoryDisplay/source/res/layout/source_activity.xml b/tests/AccessoryDisplay/source/res/layout/source_activity.xml
new file mode 100644
index 0000000..ff2b818
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/layout/source_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+    <TextView android:id="@+id/logTextView"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+</LinearLayout>
diff --git a/tests/AccessoryDisplay/source/res/values/strings.xml b/tests/AccessoryDisplay/source/res/values/strings.xml
new file mode 100644
index 0000000..0b488df
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">Accessory Display Source</string>
+</resources>
diff --git a/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml b/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml
new file mode 100644
index 0000000..5313b4e
--- /dev/null
+++ b/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!-- Match all devices -->
+    <usb-accessory manufacturer="Android" model="Accessory Display" />
+</resources>
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java
new file mode 100644
index 0000000..ccead44
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source;
+
+import com.android.accessorydisplay.common.Protocol;
+import com.android.accessorydisplay.common.Service;
+import com.android.accessorydisplay.common.Transport;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Display;
+import android.view.Surface;
+
+import java.nio.ByteBuffer;
+
+public class DisplaySourceService extends Service {
+    private static final int MSG_DISPATCH_DISPLAY_ADDED = 1;
+    private static final int MSG_DISPATCH_DISPLAY_REMOVED = 2;
+
+    private static final String DISPLAY_NAME = "Accessory Display";
+    private static final int BIT_RATE = 6000000;
+    private static final int FRAME_RATE = 30;
+    private static final int I_FRAME_INTERVAL = 10;
+
+    private final Callbacks mCallbacks;
+    private final ServiceHandler mHandler;
+    private final DisplayManager mDisplayManager;
+
+    private boolean mSinkAvailable;
+    private int mSinkWidth;
+    private int mSinkHeight;
+    private int mSinkDensityDpi;
+
+    private VirtualDisplayThread mVirtualDisplayThread;
+
+    public DisplaySourceService(Context context, Transport transport, Callbacks callbacks) {
+        super(context, transport, Protocol.DisplaySourceService.ID);
+        mCallbacks = callbacks;
+        mHandler = new ServiceHandler();
+        mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
+    }
+
+    @Override
+    public void start() {
+        super.start();
+
+        getLogger().log("Sending MSG_QUERY.");
+        getTransport().sendMessage(Protocol.DisplaySinkService.ID,
+                Protocol.DisplaySinkService.MSG_QUERY, null);
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+
+        handleSinkNotAvailable();
+    }
+
+    @Override
+    public void onMessageReceived(int service, int what, ByteBuffer content) {
+        switch (what) {
+            case Protocol.DisplaySourceService.MSG_SINK_AVAILABLE: {
+                getLogger().log("Received MSG_SINK_AVAILABLE");
+                if (content.remaining() >= 12) {
+                    final int width = content.getInt();
+                    final int height = content.getInt();
+                    final int densityDpi = content.getInt();
+                    if (width >= 0 && width <= 4096
+                            && height >= 0 && height <= 4096
+                            && densityDpi >= 60 && densityDpi <= 640) {
+                        handleSinkAvailable(width, height, densityDpi);
+                        return;
+                    }
+                }
+                getLogger().log("Receive invalid MSG_SINK_AVAILABLE message.");
+                break;
+            }
+
+            case Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE: {
+                getLogger().log("Received MSG_SINK_NOT_AVAILABLE");
+                handleSinkNotAvailable();
+                break;
+            }
+        }
+    }
+
+    private void handleSinkAvailable(int width, int height, int densityDpi) {
+        if (mSinkAvailable && mSinkWidth == width && mSinkHeight == height
+                && mSinkDensityDpi == densityDpi) {
+            return;
+        }
+
+        getLogger().log("Accessory display sink available: "
+                + "width=" + width + ", height=" + height
+                + ", densityDpi=" + densityDpi);
+        mSinkAvailable = true;
+        mSinkWidth = width;
+        mSinkHeight = height;
+        mSinkDensityDpi = densityDpi;
+        createVirtualDisplay();
+    }
+
+    private void handleSinkNotAvailable() {
+        getLogger().log("Accessory display sink not available.");
+
+        mSinkAvailable = false;
+        mSinkWidth = 0;
+        mSinkHeight = 0;
+        mSinkDensityDpi = 0;
+        releaseVirtualDisplay();
+    }
+
+    private void createVirtualDisplay() {
+        releaseVirtualDisplay();
+
+        mVirtualDisplayThread = new VirtualDisplayThread(
+                mSinkWidth, mSinkHeight, mSinkDensityDpi);
+        mVirtualDisplayThread.start();
+    }
+
+    private void releaseVirtualDisplay() {
+        if (mVirtualDisplayThread != null) {
+            mVirtualDisplayThread.quit();
+            mVirtualDisplayThread = null;
+        }
+    }
+
+    public interface Callbacks {
+        public void onDisplayAdded(Display display);
+        public void onDisplayRemoved(Display display);
+    }
+
+    private final class ServiceHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_DISPATCH_DISPLAY_ADDED: {
+                    mCallbacks.onDisplayAdded((Display)msg.obj);
+                    break;
+                }
+
+                case MSG_DISPATCH_DISPLAY_REMOVED: {
+                    mCallbacks.onDisplayRemoved((Display)msg.obj);
+                    break;
+                }
+            }
+        }
+    }
+
+    private final class VirtualDisplayThread extends Thread {
+        private static final int TIMEOUT_USEC = 1000000;
+
+        private final int mWidth;
+        private final int mHeight;
+        private final int mDensityDpi;
+
+        private volatile boolean mQuitting;
+
+        public VirtualDisplayThread(int width, int height, int densityDpi) {
+            mWidth = width;
+            mHeight = height;
+            mDensityDpi = densityDpi;
+        }
+
+        @Override
+        public void run() {
+            MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
+            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
+            format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
+            format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
+            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
+
+            MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
+            codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+            Surface surface = codec.createInputSurface();
+            codec.start();
+
+            VirtualDisplay virtualDisplay = mDisplayManager.createPrivateVirtualDisplay(
+                    DISPLAY_NAME, mWidth, mHeight, mDensityDpi, surface);
+            if (virtualDisplay != null) {
+                mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_ADDED,
+                        virtualDisplay.getDisplay()).sendToTarget();
+
+                stream(codec);
+
+                mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_REMOVED,
+                        virtualDisplay.getDisplay()).sendToTarget();
+                virtualDisplay.release();
+            }
+
+            codec.signalEndOfInputStream();
+            codec.stop();
+        }
+
+        public void quit() {
+            mQuitting = true;
+        }
+
+        private void stream(MediaCodec codec) {
+            BufferInfo info = new BufferInfo();
+            ByteBuffer[] buffers = null;
+            while (!mQuitting) {
+                int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC);
+                if (index >= 0) {
+                    if (buffers == null) {
+                        buffers = codec.getOutputBuffers();
+                    }
+
+                    ByteBuffer buffer = buffers[index];
+                    buffer.limit(info.offset + info.size);
+                    buffer.position(info.offset);
+
+                    getTransport().sendMessage(Protocol.DisplaySinkService.ID,
+                            Protocol.DisplaySinkService.MSG_CONTENT, buffer);
+                    codec.releaseOutputBuffer(index, false);
+                } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+                    buffers = null;
+                } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
+                    getLogger().log("Codec dequeue buffer timed out.");
+                }
+            }
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java
new file mode 100644
index 0000000..c59c958
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source;
+
+import com.android.accessorydisplay.common.Logger;
+import com.android.accessorydisplay.source.presentation.DemoPresentation;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbAccessory;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.Display;
+import android.widget.TextView;
+
+public class SourceActivity extends Activity {
+    private static final String TAG = "SourceActivity";
+
+    private static final String ACTION_USB_ACCESSORY_PERMISSION =
+            "com.android.accessorydisplay.source.ACTION_USB_ACCESSORY_PERMISSION";
+
+    private static final String MANUFACTURER = "Android";
+    private static final String MODEL = "Accessory Display";
+
+    private UsbManager mUsbManager;
+    private AccessoryReceiver mReceiver;
+    private TextView mLogTextView;
+    private Logger mLogger;
+    private Presenter mPresenter;
+
+    private boolean mConnected;
+    private UsbAccessory mAccessory;
+    private UsbAccessoryStreamTransport mTransport;
+
+    private DisplaySourceService mDisplaySourceService;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE);
+
+        setContentView(R.layout.source_activity);
+
+        mLogTextView = (TextView) findViewById(R.id.logTextView);
+        mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance());
+        mLogger = new TextLogger();
+        mPresenter = new Presenter();
+
+        mLogger.log("Waiting for accessory display sink to be attached to USB...");
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
+        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
+        filter.addAction(ACTION_USB_ACCESSORY_PERMISSION);
+        mReceiver = new AccessoryReceiver();
+        registerReceiver(mReceiver, filter);
+
+        Intent intent = getIntent();
+        if (intent.getAction().equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) {
+            UsbAccessory accessory =
+                    (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
+            if (accessory != null) {
+                onAccessoryAttached(accessory);
+            }
+        } else {
+            UsbAccessory[] accessories = mUsbManager.getAccessoryList();
+            if (accessories != null) {
+                for (UsbAccessory accessory : accessories) {
+                    onAccessoryAttached(accessory);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        unregisterReceiver(mReceiver);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        //new DemoPresentation(this, getWindowManager().getDefaultDisplay()).show();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    private void onAccessoryAttached(UsbAccessory accessory) {
+        mLogger.log("USB accessory attached: " + accessory);
+        if (!mConnected) {
+            connect(accessory);
+        }
+    }
+
+    private void onAccessoryDetached(UsbAccessory accessory) {
+        mLogger.log("USB accessory detached: " + accessory);
+        if (mConnected && accessory.equals(mAccessory)) {
+            disconnect();
+        }
+    }
+
+    private void connect(UsbAccessory accessory) {
+        if (!isSink(accessory)) {
+            mLogger.log("Not connecting to USB accessory because it is not an accessory display sink: "
+                    + accessory);
+            return;
+        }
+
+        if (mConnected) {
+            disconnect();
+        }
+
+        // Check whether we have permission to access the accessory.
+        if (!mUsbManager.hasPermission(accessory)) {
+            mLogger.log("Prompting the user for access to the accessory.");
+            Intent intent = new Intent(ACTION_USB_ACCESSORY_PERMISSION);
+            intent.setPackage(getPackageName());
+            PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+            mUsbManager.requestPermission(accessory, pendingIntent);
+            return;
+        }
+
+        // Open the accessory.
+        ParcelFileDescriptor fd = mUsbManager.openAccessory(accessory);
+        if (fd == null) {
+            mLogger.logError("Could not obtain accessory connection.");
+            return;
+        }
+
+        // All set.
+        mLogger.log("Connected.");
+        mConnected = true;
+        mAccessory = accessory;
+        mTransport = new UsbAccessoryStreamTransport(mLogger, fd);
+        startServices();
+        mTransport.startReading();
+    }
+
+    private void disconnect() {
+        mLogger.log("Disconnecting from accessory: " + mAccessory);
+        stopServices();
+
+        mLogger.log("Disconnected.");
+        mConnected = false;
+        mAccessory = null;
+        if (mTransport != null) {
+            mTransport.close();
+            mTransport = null;
+        }
+    }
+
+    private void startServices() {
+        mDisplaySourceService = new DisplaySourceService(this, mTransport, mPresenter);
+        mDisplaySourceService.start();
+    }
+
+    private void stopServices() {
+        if (mDisplaySourceService != null) {
+            mDisplaySourceService.stop();
+            mDisplaySourceService = null;
+        }
+    }
+
+    private static boolean isSink(UsbAccessory accessory) {
+        return MANUFACTURER.equals(accessory.getManufacturer())
+                && MODEL.equals(accessory.getModel());
+    }
+
+    class TextLogger extends Logger {
+        @Override
+        public void log(final String message) {
+            Log.d(TAG, message);
+
+            mLogTextView.post(new Runnable() {
+                @Override
+                public void run() {
+                    mLogTextView.append(message);
+                    mLogTextView.append("\n");
+                }
+            });
+        }
+    }
+
+    class AccessoryReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            UsbAccessory accessory = intent.<UsbAccessory>getParcelableExtra(
+                    UsbManager.EXTRA_ACCESSORY);
+            if (accessory != null) {
+                String action = intent.getAction();
+                if (action.equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) {
+                    onAccessoryAttached(accessory);
+                } else if (action.equals(UsbManager.ACTION_USB_ACCESSORY_DETACHED)) {
+                    onAccessoryDetached(accessory);
+                } else if (action.equals(ACTION_USB_ACCESSORY_PERMISSION)) {
+                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
+                        mLogger.log("Accessory permission granted: " + accessory);
+                        onAccessoryAttached(accessory);
+                    } else {
+                        mLogger.logError("Accessory permission denied: " + accessory);
+                    }
+                }
+            }
+        }
+    }
+
+    class Presenter implements DisplaySourceService.Callbacks {
+        private DemoPresentation mPresentation;
+
+        @Override
+        public void onDisplayAdded(Display display) {
+            mLogger.log("Accessory display added: " + display);
+
+            mPresentation = new DemoPresentation(SourceActivity.this, display, mLogger);
+            mPresentation.show();
+        }
+
+        @Override
+        public void onDisplayRemoved(Display display) {
+            mLogger.log("Accessory display removed: " + display);
+
+            if (mPresentation != null) {
+                mPresentation.dismiss();
+                mPresentation = null;
+            }
+        }
+    }
+}
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java
new file mode 100644
index 0000000..c28f4359
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source;
+
+import com.android.accessorydisplay.common.Logger;
+import com.android.accessorydisplay.common.Transport;
+
+import android.hardware.usb.UsbAccessory;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Sends or receives messages over a file descriptor associated with a {@link UsbAccessory}.
+ */
+public class UsbAccessoryStreamTransport extends Transport {
+    private ParcelFileDescriptor mFd;
+    private FileInputStream mInputStream;
+    private FileOutputStream mOutputStream;
+
+    public UsbAccessoryStreamTransport(Logger logger, ParcelFileDescriptor fd) {
+        super(logger, 16384);
+        mFd = fd;
+        mInputStream = new FileInputStream(fd.getFileDescriptor());
+        mOutputStream = new FileOutputStream(fd.getFileDescriptor());
+    }
+
+    @Override
+    protected void ioClose() {
+        try {
+            mFd.close();
+        } catch (IOException ex) {
+        }
+        mFd = null;
+        mInputStream = null;
+        mOutputStream = null;
+    }
+
+    @Override
+    protected int ioRead(byte[] buffer, int offset, int count) throws IOException {
+        if (mInputStream == null) {
+            throw new IOException("Stream was closed.");
+        }
+        return mInputStream.read(buffer, offset, count);
+    }
+
+    @Override
+    protected void ioWrite(byte[] buffer, int offset, int count) throws IOException {
+        if (mOutputStream == null) {
+            throw new IOException("Stream was closed.");
+        }
+        mOutputStream.write(buffer, offset, count);
+    }
+}
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java
new file mode 100644
index 0000000..51d8da9
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source.presentation;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * A vertex shaded cube.
+ */
+class Cube
+{
+    public Cube()
+    {
+        int one = 0x10000;
+        int vertices[] = {
+                -one, -one, -one,
+                one, -one, -one,
+                one,  one, -one,
+                -one,  one, -one,
+                -one, -one,  one,
+                one, -one,  one,
+                one,  one,  one,
+                -one,  one,  one,
+        };
+
+        int colors[] = {
+                0,    0,    0,  one,
+                one,    0,    0,  one,
+                one,  one,    0,  one,
+                0,  one,    0,  one,
+                0,    0,  one,  one,
+                one,    0,  one,  one,
+                one,  one,  one,  one,
+                0,  one,  one,  one,
+        };
+
+        byte indices[] = {
+                0, 4, 5,    0, 5, 1,
+                1, 5, 6,    1, 6, 2,
+                2, 6, 7,    2, 7, 3,
+                3, 7, 4,    3, 4, 0,
+                4, 7, 6,    4, 6, 5,
+                3, 0, 1,    3, 1, 2
+        };
+
+        // Buffers to be passed to gl*Pointer() functions
+        // must be direct, i.e., they must be placed on the
+        // native heap where the garbage collector cannot
+        // move them.
+        //
+        // Buffers with multi-byte datatypes (e.g., short, int, float)
+        // must have their byte order set to native order
+
+        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
+        vbb.order(ByteOrder.nativeOrder());
+        mVertexBuffer = vbb.asIntBuffer();
+        mVertexBuffer.put(vertices);
+        mVertexBuffer.position(0);
+
+        ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
+        cbb.order(ByteOrder.nativeOrder());
+        mColorBuffer = cbb.asIntBuffer();
+        mColorBuffer.put(colors);
+        mColorBuffer.position(0);
+
+        mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
+        mIndexBuffer.put(indices);
+        mIndexBuffer.position(0);
+    }
+
+    public void draw(GL10 gl)
+    {
+        gl.glFrontFace(GL10.GL_CW);
+        gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
+        gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
+        gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer);
+    }
+
+    private IntBuffer   mVertexBuffer;
+    private IntBuffer   mColorBuffer;
+    private ByteBuffer  mIndexBuffer;
+}
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java
new file mode 100644
index 0000000..51dc82a
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source.presentation;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.opengl.GLSurfaceView;
+
+/**
+ * Render a pair of tumbling cubes.
+ */
+
+public class CubeRenderer implements GLSurfaceView.Renderer {
+    private boolean mTranslucentBackground;
+    private Cube mCube;
+    private float mAngle;
+    private float mScale = 1.0f;
+    private boolean mExploding;
+
+    public CubeRenderer(boolean useTranslucentBackground) {
+        mTranslucentBackground = useTranslucentBackground;
+        mCube = new Cube();
+    }
+
+    public void explode() {
+        mExploding = true;
+    }
+
+    public void onDrawFrame(GL10 gl) {
+        /*
+         * Usually, the first thing one might want to do is to clear
+         * the screen. The most efficient way of doing this is to use
+         * glClear().
+         */
+
+        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+
+        /*
+         * Now we're ready to draw some 3D objects
+         */
+
+        gl.glMatrixMode(GL10.GL_MODELVIEW);
+        gl.glLoadIdentity();
+        gl.glTranslatef(0, 0, -3.0f);
+        gl.glRotatef(mAngle,        0, 1, 0);
+        gl.glRotatef(mAngle*0.25f,  1, 0, 0);
+
+        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
+        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
+
+        gl.glScalef(mScale, mScale, mScale);
+        mCube.draw(gl);
+
+        gl.glRotatef(mAngle*2.0f, 0, 1, 1);
+        gl.glTranslatef(0.5f, 0.5f, 0.5f);
+
+        mCube.draw(gl);
+
+        mAngle += 1.2f;
+
+        if (mExploding) {
+            mScale *= 1.02f;
+            if (mScale > 4.0f) {
+                mScale = 1.0f;
+                mExploding = false;
+            }
+        }
+    }
+
+    public void onSurfaceChanged(GL10 gl, int width, int height) {
+         gl.glViewport(0, 0, width, height);
+
+         /*
+          * Set our projection matrix. This doesn't have to be done
+          * each time we draw, but usually a new projection needs to
+          * be set when the viewport is resized.
+          */
+
+         float ratio = (float) width / height;
+         gl.glMatrixMode(GL10.GL_PROJECTION);
+         gl.glLoadIdentity();
+         gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+    }
+
+    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+        /*
+         * By default, OpenGL enables features that improve quality
+         * but reduce performance. One might want to tweak that
+         * especially on software renderer.
+         */
+        gl.glDisable(GL10.GL_DITHER);
+
+        /*
+         * Some one-time OpenGL initialization can be made here
+         * probably based on features of this particular context
+         */
+         gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
+                 GL10.GL_FASTEST);
+
+         if (mTranslucentBackground) {
+             gl.glClearColor(0,0,0,0);
+         } else {
+             gl.glClearColor(1,1,1,1);
+         }
+         gl.glEnable(GL10.GL_CULL_FACE);
+         gl.glShadeModel(GL10.GL_SMOOTH);
+         gl.glEnable(GL10.GL_DEPTH_TEST);
+    }
+}
diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java
new file mode 100644
index 0000000..517b7fc
--- /dev/null
+++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.accessorydisplay.source.presentation;
+
+import com.android.accessorydisplay.common.Logger;
+import com.android.accessorydisplay.source.R;
+
+import android.app.Presentation;
+import android.content.Context;
+import android.content.res.Resources;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Button;
+
+/**
+ * The presentation to show on the accessory display.
+ * <p>
+ * Note that this display may have different metrics from the display on which
+ * the main activity is showing so we must be careful to use the presentation's
+ * own {@link Context} whenever we load resources.
+ * </p>
+ */
+public final class DemoPresentation extends Presentation {
+    private final Logger mLogger;
+
+    private GLSurfaceView mSurfaceView;
+    private CubeRenderer mRenderer;
+    private Button mExplodeButton;
+
+    public DemoPresentation(Context context, Display display, Logger logger) {
+        super(context, display);
+        mLogger = logger;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // Be sure to call the super class.
+        super.onCreate(savedInstanceState);
+
+        // Get the resources for the context of the presentation.
+        // Notice that we are getting the resources from the context of the presentation.
+        Resources r = getContext().getResources();
+
+        // Inflate the layout.
+        setContentView(R.layout.presentation_content);
+
+        // Set up the surface view for visual interest.
+        mRenderer = new CubeRenderer(false);
+        mSurfaceView = (GLSurfaceView)findViewById(R.id.surface_view);
+        mSurfaceView.setRenderer(mRenderer);
+
+        // Add a button.
+        mExplodeButton = (Button)findViewById(R.id.explode_button);
+        mExplodeButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mRenderer.explode();
+            }
+        });
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        mLogger.log("Received touch event: " + event);
+        return super.onTouchEvent(event);
+    }
+}
\ No newline at end of file