From 8f3b1307678fcd1896c7fb8ba4cc20553dc032e8 Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Wed, 29 May 2013 14:59:46 -0700 Subject: [PATCH] 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 --- tests/AccessoryDisplay/Android.mk | 17 + tests/AccessoryDisplay/README | 50 ++ tests/AccessoryDisplay/common/Android.mk | 23 + .../accessorydisplay/common/BufferPool.java | 92 ++++ .../accessorydisplay/common/Logger.java | 25 + .../accessorydisplay/common/Protocol.java | 65 +++ .../accessorydisplay/common/Service.java | 71 +++ .../accessorydisplay/common/Transport.java | 382 +++++++++++++ tests/AccessoryDisplay/sink/Android.mk | 25 + .../AccessoryDisplay/sink/AndroidManifest.xml | 41 ++ .../sink/res/drawable-hdpi/ic_app.png | Bin 0 -> 3608 bytes .../sink/res/drawable-mdpi/ic_app.png | Bin 0 -> 5198 bytes .../sink/res/layout/sink_activity.xml | 44 ++ .../sink/res/values/strings.xml | 19 + .../sink/res/xml/usb_device_filter.xml | 25 + .../sink/DisplaySinkService.java | 240 +++++++++ .../accessorydisplay/sink/SinkActivity.java | 508 ++++++++++++++++++ .../sink/UsbAccessoryBulkTransport.java | 73 +++ .../sink/UsbAccessoryConstants.java | 135 +++++ .../android/accessorydisplay/sink/UsbHid.java | 130 +++++ tests/AccessoryDisplay/source/Android.mk | 25 + .../source/AndroidManifest.xml | 41 ++ .../source/res/drawable-hdpi/ic_app.png | Bin 0 -> 3608 bytes .../source/res/drawable-mdpi/ic_app.png | Bin 0 -> 5198 bytes .../res/layout/presentation_content.xml | 30 ++ .../source/res/layout/source_activity.xml | 24 + .../source/res/values/strings.xml | 19 + .../source/res/xml/usb_accessory_filter.xml | 20 + .../source/DisplaySourceService.java | 246 +++++++++ .../source/SourceActivity.java | 257 +++++++++ .../source/UsbAccessoryStreamTransport.java | 70 +++ .../source/presentation/Cube.java | 100 ++++ .../source/presentation/CubeRenderer.java | 124 +++++ .../source/presentation/DemoPresentation.java | 84 +++ 34 files changed, 3005 insertions(+) create mode 100644 tests/AccessoryDisplay/Android.mk create mode 100644 tests/AccessoryDisplay/README create mode 100644 tests/AccessoryDisplay/common/Android.mk create mode 100644 tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java create mode 100644 tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java create mode 100644 tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java create mode 100644 tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java create mode 100644 tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java create mode 100644 tests/AccessoryDisplay/sink/Android.mk create mode 100644 tests/AccessoryDisplay/sink/AndroidManifest.xml create mode 100755 tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png create mode 100644 tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png create mode 100644 tests/AccessoryDisplay/sink/res/layout/sink_activity.xml create mode 100644 tests/AccessoryDisplay/sink/res/values/strings.xml create mode 100644 tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml create mode 100644 tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java create mode 100644 tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java create mode 100644 tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java create mode 100644 tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java create mode 100644 tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java create mode 100644 tests/AccessoryDisplay/source/Android.mk create mode 100644 tests/AccessoryDisplay/source/AndroidManifest.xml create mode 100755 tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png create mode 100644 tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png create mode 100644 tests/AccessoryDisplay/source/res/layout/presentation_content.xml create mode 100644 tests/AccessoryDisplay/source/res/layout/source_activity.xml create mode 100644 tests/AccessoryDisplay/source/res/values/strings.xml create mode 100644 tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java create mode 100644 tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java diff --git a/tests/AccessoryDisplay/Android.mk b/tests/AccessoryDisplay/Android.mk new file mode 100644 index 0000000000000..85cb309bd7208 --- /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 0000000000000..5ce558cfe1942 --- /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 0000000000000..2d4de15645063 --- /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 0000000000000..a6bb5c1b91892 --- /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 0000000000000..e0b7e8216a5a3 --- /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 0000000000000..46fee325e3a56 --- /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 0000000000000..70b380635bcdf --- /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. + *

+ * 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. + *

+ */ +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 0000000000000..84897d372e1e8 --- /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. + *

+ * This object's interface is thread-safe, however incoming messages + * are always delivered on the {@link Looper} thread on which the transport + * was created. + *

+ */ +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 mServices = new SparseArray(); + + 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 0000000000000..772ce0c849fb2 --- /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 0000000000000..72d498f2d855a --- /dev/null +++ b/tests/AccessoryDisplay/sink/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..66a198496cfb3e77bffb2ca97007e30b2837106a GIT binary patch literal 3608 zcmV+z4(IWSP)>Yjs+!VGP=X>Hec-`Em6es-s;jFtd9cIbfR>gP zFdB^q@$jX-zVr9g)^3~Cop40VIQk#ix^=7O4QOs|hK7cQj_T@~7f_u|Isg&LF_}z_ z>({T(N+b5{*#q_U_4hvd*tdUzAQ*3S6U4Q(cT_u_&N6hsS!+mBQxkoB&t!U{gXj6+ zD(x@MU@+9<=OnhV7t8DI9s7U!`fIQH)dawRjHkZ1xEQiIUvD;BVDH`*sI9HNmFHJ4 zGc7GG1!QmeV`F2jmoE>rA|OXQ@4f%P%P;+DSS;z=cC$ ze<&2vdv`ep->oB z%c41SdT{0P08C7b!)nybd`BKq>E<8p+t3xOi~vBHM-W!t-QfuE`8;s` z{3lSieLLFQ2FS_DS(REaK0dzJ=kx9D>FN3Y(@#Hr3`;kj=9K_IBW^qp2&@PTiV2y( zk&$a~vA-YM+uMLrBFU^`H3{XnNXp8}^18db|BL`Uisg?uhw`}(_QRn=UxSK@3U+QSEF`A39zJ~7+|||f{8LXoHI1F$B{!1AoUnkNeMLR+ z?CdP`UpNn~t*ywlT}&I+Qb8?%0|yS6olfU-Pd@qNCOPM9N)>cuTTuJp0(O2~eI0Ds zQVpv)DwY$MYXdYkHWdZt=YN5R_h1Pwn}#l!g6x)Xn1`!_gHT*l2z7OJzzaO`;qaO) z)b?9%yA>>E^TFfCk8hUJ0+W2_3(K^nkGn|r3?=b!IdkQ!Dh3 zWHxWd0}B>^8URSM4(j|1eV>5cVTZeLv=&Dxm|7$D%MyiC|CvoDFmPO-N*hWjj>oPqW8`D{V*{(0Y-xnM`MBHeHtNv{lf;O zEo~nl)u9w0Tpb!@xqFV?!)G;Kn4Fjdw|f*YMSSt)uynaJ3^Lqk^~&u)j3 z;t~WSKO6lIAtV;j?#bK<)A7{Q6sZ(t04ZaaRh6Ebn}dOYFT~OSu)e$;N1JhpmQYNQ zDAU+}D%CYxHP;5}FB-5I+4>d40ZV-VnV;F2X|URCj520FDHoLV4<3(q$z%=zBXxfe z>oOXRngb9DvgBb{Q6MG;z&s*q;0%m1W?QUQEAu&G&-Qx8@MRarI+Q&XGoxm+8O%8P zwH`zafKZ4ST~LbBr44{U1>S-4_^k!d_K0Ad-=ccxVc@IgQpUS|h$d11Co5 z4Y3ifw$rEjf>AA7(gutdHf`FHxdu?@Q%XgU&Z(qtf*`5^STU!;w1HZqJQE>@K85P~o^__qnS0CsNG0No>(>6hLN{C() zG>C+6LT5{S{~DU2bcBK`yWPP!eoT`nO`fjlBAQ!P?o#F5VvJEv($|0}sxfn3V@KTCNp1!g6s>!3(vNh49s>pl;7(6Odlj~ee5V~n801VNPPB=hr#O#{s*$(Ivt z)2QFLu?li*R`GS029EK1d!7RZ20oXqkFFIII8}Y87;P7nX@DR}Lt}G@T4q5hjgV?y zU}NRYV6j?LwuA0jk7k7ycge{(K9f@0>H?tH2Z)&6Oz|Go-tMuGoK$?aM=={`0fEV#MvNqkK zY<;NFXi9s&nOCUoL|7sVoE6hhDH9Y14*vy1A%?6G=^IBS1GS)fv$g6u-RGp%n8@Ja z!#tZIjIEAa2*xxpK?Z{r#p;cH-n?lu zO93Pbt!7AMMg&NrCQVQGK>Pj!%P4DkV8JW1E3In^vt1C(}o*Uql(om4r z007nJ0!ous*3nen7YV6|ge0jIN2ea9mR zDw-f(8vv5UwV{HIdb0OY^4VvX7)xooW4EloIP>A@MPP~v%@b=8gr%CbRm6fwrwkec zFgh~Ao|lS+T>A7Pn?J6rZ-9!5jq$ZU)bWFZSK#bNy=bncQ$EjRGO|?_YOfXI1eF1x zi5P)nd-gT!d>XO6XFg0LZ!`h#zVmi8MpytK9igenlP6z+mX>{@nN{ToaLOlSB;x=L zHK4ouZ*XmRSn>atV=Sp7@-g^(cNgq#Zibqgt?F4Ir4us5wZ(iuL<7_ZV0dVVZO~Xr z@%v}t%{N|$t+(8wnh#Jp85d@#8Z=mwlAm9|f~&|5&6N|Hl2q&cQjrx!*-(L!5gPaa zN_sUm?S{Ai^`<0E9do-?qrFmfwe0(cGMj!3 z#I7Wq+XZ*EwX?s;JWxQDiR{_C7fTDcM@Qi=FTWID1V&O_Qv=(#Z&%X?%Q^vryaw<| z1tZI_z)&R>)v9#fdq3N6OVM#>=Y7!LzCV6#*RIBlw~^M>)w8i$lo?LVVuUkiL^GPs!a_K9>>=<+X{gO=g%@A^1KYhRR>$tY%MR&hWop|36=_VPi` z{}yf1D|fk|xM*qmV1sJ|IFZ`=e7*($VzWU@ODnsU4dGZMrqBe5uMLf4HVlT9*~Nb3 z$PqYr@F1K!dlt@}JI8=H(%C85o^L$(Ae{WiN!Ia1FB_Yhvf5rw`HYx^35u?YSjn#u z$j!|~7IOzHsieBPhLx3*3?gAM6C^}d=d$@;U6z}SOVb2}L*Zbo!;=A^tx194{E9f? zwJ0I>?J0a99RP%L=gyw5tf~s27A#qSLu*|;V;-2AoCGz0Q{c-gl;ZY!JiSu+2r)at zBi|l(yWPJ{wEHm!%o1W)>Bk>`65l7L7x8lX^oMZi(xrF0y8ikfNcvXYxQUYZ1&zF% zoSdq!-+lO*qN1YC#LlNB8?<50;jptw3;O!rYy>oGNoWdm46Q_8U*G9B-gxbYpI*Fp z22W22B0-E40JJEF)`^ri+`gmP<#IJ5^QeeAXKsFe-Z(QoWi=S-mrV+sc+I-@zuPB0 z;dx;WNy<3(*SN`O3ZPF6$7X;rV03%Ep1#*!{pU$Mq^&*_h=fEJamoCK41tgr1%lRr zTM|qbecGbu>C+51)-N&KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000SjNklbYyEZE9Mc4JAP*D(4R|QCEg?k3o05RKuYT?8CxXG?&jW$L!EM{N*&0w) zRVF7V=i~ACYXg6K>-WGEkg|Xf4?P1cbAR(&-~K^s>yB5CA3shc5^*%3s;XQ1`}=sCzDbTpvb?dIgklT1uZe1Bx* zDotxu9u>bxQ$Ic6M}pBNmGVgTWwcEtG=6ASX_osLSW`zepyN z?|VI-S(nR|N7^_yFbpG|PN#3Y^Ut^60W!e6r9=8kKr915lH^!07#yE%30 zRA_vB{N#h8^YrvI9UYz7PjBA*^}Fx>>kZ%@Ft?HhEd#(bO_va2-2eczwzksR+WJ6! zdTwscKQJ)xOI6J`UA}zj3@~a5v1UsHgfL|x1dnQ;AP@-9*Vo7R-McSezI^EeOM9xi zf~#75e3Tm{p-_k&ZEeB!_MOM>+!?zEBmuP&2(D^!S_>Cg6dnr&Zr{2^CX;y}AQTD( zcK7u3HE-LdU-|b3b5>MHSwI*o0zi!!P$(1_7#JXz%MyvSv$H*d&+p$*0GdNhL?V&S zk&%&CdV2P}aPI8ie-w|$KeUpw>;ec?U7(f>xOVM2E|-g+{q!d^H8nj@cbJ)(;g5g% zbDPWM{@v?mUi+c-?+G9Wh!p^+4FgiC6r-b~ynOl;KA(?VF84qI@cDea^1bi!n^%7u zK6voZcP?JM@CPeTC;)nO4G@(Xkk94F<@2^CPb3oD8okBdy}g9PEf4(W%WZFOFN1@F zp8?u{yOuEO)iq!V1E!~^85tQNpU)9)X|X-=GlvfF*{7aD&8z>@$l&pKkiZ8t0e&kz zUqJ)P7%-EVVQ6TG{rmQ^r)Rh0$u(_p4x;Hg*=&|fW*%9VX$>z1bS{%2nM~pJdI$sp z_`KfQG(QwY20T`IuhLCML+2xlv$?AFtO}I*(7q(RH0% zHp}eHENyK&sH^j=dtODBC94S}t&#}cPZu(o3~qNFot-<;4ZS8$!Y~aAWvrN-iqqD% z1AugTj_eYxkQHV9b4wDkqS)#OAh^!}0W>z$qicH2{85%<3I(++Pf$sxQ&oMp7IRC0 zVjBP-?kvkO{9F9CGfPR9 zkQLd928cxr5Dpoj>pGgIV;BY-Wu{@;UY9J%D6$g`D7kovdm*b`6C6l$`)E?JiByFhVFQPNLEk=NAJ_WS+p+O>;o*RNG?N-8ee+S;gZs4tey zd6mh@dn8k-s^4kb(MEG~v!gZ1vTR=jmli{cqLif$&MZBbMz11zfk2S`pWBbiRkn%HmrV$L)66UWeoa0Nv0_sjAE6A`sYu6AHX{{ye!{mRz=a^%se><954O948u! zGJf}N#cTf<7{KLrv2|-b?d|PE+S_Z#08P_MrK`u|L6RhN-9T|ENUPv59iL`$a&lFW zwzssbis{z+dbAY;RMT~Gxjc>C>lC3k7Bp3C^ECPa=_6_C&8x_(e}zye zL@*d|6bWnsU{Fw1iw$~32CSL`_3@4t7RFFp5M#qSRf5A)&m z>lLpBgF#{r!=Q5jEC%Shwk7~d8CxvIrHdD<-sJT3GzSkIA{L7kn<4QySFc=QczC$# zm|fB6n${#s4ggr7o?#djk+Wl}s&ek^*%jY0#N%<^dh<<3;||@p!IMuuiQSDGinY?AfD7Q40mU9uEf&9>C-A(%ak1*>mR@yM5c%7+$ZJ zlP|tVcXzkd7#ZWz#fyCT`R5(4%QgVzug8TDYX+bgua7S}54X0`+0kJefR>gPdU|?F z=bfFMbar;Gu`#@621qsa-_(*?zoWB*W5TM@R9ghQqB44h}AQags`O97B6ZjTQE+bM1)@cy3wkOyWcZjwp>c_DvpyR_h{Z1OINIiKhBg7%1Yi?@$J+io0NzSjgnf4iF8}}l07*qo IM6N<$f@LhcdjJ3c literal 0 HcmV?d00001 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 0000000000000..6afb850bf8f84 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/tests/AccessoryDisplay/sink/res/values/strings.xml b/tests/AccessoryDisplay/sink/res/values/strings.xml new file mode 100644 index 0000000000000..29cd001ed2d2c --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + Accessory Display Sink + 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 0000000000000..e8fe9291d6c47 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + 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 0000000000000..daec845d21e01 --- /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 0000000000000..6fe2cfbcd8f40 --- /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.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + onDeviceAttached(device); + } + } else { + Map 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.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 0000000000000..a15bfadec8590 --- /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 0000000000000..8197d6b972ac9 --- /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 0000000000000..b4fa1fd9b8d32 --- /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 0000000000000..5d1085dc2a847 --- /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 0000000000000..d3edcb8388771 --- /dev/null +++ b/tests/AccessoryDisplay/source/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..66a198496cfb3e77bffb2ca97007e30b2837106a GIT binary patch literal 3608 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 IM6N<$f@LhcdjJ3c literal 0 HcmV?d00001 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 0000000000000..bf9566a922bca --- /dev/null +++ b/tests/AccessoryDisplay/source/res/layout/presentation_content.xml @@ -0,0 +1,30 @@ + + + + + + +