summaryrefslogtreecommitdiffstats
path: root/tests/AccessoryDisplay/common
diff options
context:
space:
mode:
authorJeff Brown <jeffbrown@google.com>2013-05-29 14:59:46 -0700
committerJeff Brown <jeffbrown@google.com>2013-06-18 15:32:41 -0700
commit8f3b1307678fcd1896c7fb8ba4cc20553dc032e8 (patch)
tree9935dd8edf3a8380256502a132d040a4fd607df8 /tests/AccessoryDisplay/common
parenta506a6ec94863a35acca9feb165db76ddac3892c (diff)
downloadframeworks_base-8f3b1307678fcd1896c7fb8ba4cc20553dc032e8.zip
frameworks_base-8f3b1307678fcd1896c7fb8ba4cc20553dc032e8.tar.gz
frameworks_base-8f3b1307678fcd1896c7fb8ba4cc20553dc032e8.tar.bz2
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
Diffstat (limited to 'tests/AccessoryDisplay/common')
-rw-r--r--tests/AccessoryDisplay/common/Android.mk23
-rw-r--r--tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java92
-rw-r--r--tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java25
-rw-r--r--tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java65
-rw-r--r--tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java71
-rw-r--r--tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java382
6 files changed, 658 insertions, 0 deletions
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;
+ }
+ }
+}