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
This commit is contained in:
Jeff Brown
2013-05-29 14:59:46 -07:00
parent a506a6ec94
commit 8f3b130767
34 changed files with 3005 additions and 0 deletions

View File

@@ -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))

View File

@@ -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.

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.accessorydisplay.sink" >
<uses-feature android:name="android.hardware.usb.host"/>
<uses-sdk android:minSdkVersion="18" />
<application android:label="@string/app_name"
android:icon="@drawable/ic_app"
android:hardwareAccelerated="true">
<activity android:name=".SinkActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/usb_device_filter"/>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView android:id="@+id/fpsTextView"
android:layout_width="match_parent"
android:layout_height="64dp"
android:padding="4dp" />
<TextView android:id="@+id/logTextView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<SurfaceView android:id="@+id/surfaceView"
android:layout_width="640px"
android:layout_height="480px" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Accessory Display Sink</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Match all devices -->
<usb-device />
<!-- Android USB accessory: accessory -->
<usb-device vendor-id="16601" product-id="11520" />
<!-- Android USB accessory: accessory + adb -->
<usb-device vendor-id="16601" product-id="11521" />
</resources>

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,508 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.sink;
import com.android.accessorydisplay.common.Logger;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaFormat;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Map;
public class SinkActivity extends Activity {
private static final String TAG = "SinkActivity";
private static final String ACTION_USB_DEVICE_PERMISSION =
"com.android.accessorydisplay.sink.ACTION_USB_DEVICE_PERMISSION";
private static final String MANUFACTURER = "Android";
private static final String MODEL = "Accessory Display";
private static final String DESCRIPTION = "Accessory Display Sink Test Application";
private static final String VERSION = "1.0";
private static final String URI = "http://www.android.com/";
private static final String SERIAL = "0000000012345678";
private static final int MULTITOUCH_DEVICE_ID = 0;
private static final int MULTITOUCH_REPORT_ID = 1;
private static final int MULTITOUCH_MAX_CONTACTS = 1;
private UsbManager mUsbManager;
private DeviceReceiver mReceiver;
private TextView mLogTextView;
private TextView mFpsTextView;
private SurfaceView mSurfaceView;
private Logger mLogger;
private boolean mConnected;
private int mProtocolVersion;
private UsbDevice mDevice;
private UsbInterface mAccessoryInterface;
private UsbDeviceConnection mAccessoryConnection;
private UsbEndpoint mControlEndpoint;
private UsbAccessoryBulkTransport mTransport;
private boolean mAttached;
private DisplaySinkService mDisplaySinkService;
private final ByteBuffer mHidBuffer = ByteBuffer.allocate(4096);
private UsbHid.Multitouch mMultitouch;
private boolean mMultitouchEnabled;
private UsbHid.Multitouch.Contact[] mMultitouchContacts;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE);
setContentView(R.layout.sink_activity);
mLogTextView = (TextView) findViewById(R.id.logTextView);
mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance());
mLogger = new TextLogger();
mFpsTextView = (TextView) findViewById(R.id.fpsTextView);
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
sendHidTouch(event);
return true;
}
});
mLogger.log("Waiting for accessory display source to be attached to USB...");
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(ACTION_USB_DEVICE_PERMISSION);
mReceiver = new DeviceReceiver();
registerReceiver(mReceiver, filter);
Intent intent = getIntent();
if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device != null) {
onDeviceAttached(device);
}
} else {
Map<String, UsbDevice> devices = mUsbManager.getDeviceList();
if (devices != null) {
for (UsbDevice device : devices.values()) {
onDeviceAttached(device);
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
private void onDeviceAttached(UsbDevice device) {
mLogger.log("USB device attached: " + device);
if (!mConnected) {
connect(device);
}
}
private void onDeviceDetached(UsbDevice device) {
mLogger.log("USB device detached: " + device);
if (mConnected && device.equals(mDevice)) {
disconnect();
}
}
private void connect(UsbDevice device) {
if (mConnected) {
disconnect();
}
// Check whether we have permission to access the device.
if (!mUsbManager.hasPermission(device)) {
mLogger.log("Prompting the user for access to the device.");
Intent intent = new Intent(ACTION_USB_DEVICE_PERMISSION);
intent.setPackage(getPackageName());
PendingIntent pendingIntent = PendingIntent.getBroadcast(
this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
mUsbManager.requestPermission(device, pendingIntent);
return;
}
// Claim the device.
UsbDeviceConnection conn = mUsbManager.openDevice(device);
if (conn == null) {
mLogger.logError("Could not obtain device connection.");
return;
}
UsbInterface iface = device.getInterface(0);
UsbEndpoint controlEndpoint = iface.getEndpoint(0);
if (!conn.claimInterface(iface, true)) {
mLogger.logError("Could not claim interface.");
return;
}
try {
// If already in accessory mode, then connect to the device.
if (isAccessory(device)) {
mLogger.log("Connecting to accessory...");
int protocolVersion = getProtocol(conn);
if (protocolVersion < 1) {
mLogger.logError("Device does not support accessory protocol.");
return;
}
mLogger.log("Protocol version: " + protocolVersion);
// Setup bulk endpoints.
UsbEndpoint bulkIn = null;
UsbEndpoint bulkOut = null;
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint ep = iface.getEndpoint(i);
if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
if (bulkIn == null) {
mLogger.log(String.format("Bulk IN endpoint: %d", i));
bulkIn = ep;
}
} else {
if (bulkOut == null) {
mLogger.log(String.format("Bulk OUT endpoint: %d", i));
bulkOut = ep;
}
}
}
if (bulkIn == null || bulkOut == null) {
mLogger.logError("Unable to find bulk endpoints");
return;
}
mLogger.log("Connected");
mConnected = true;
mDevice = device;
mProtocolVersion = protocolVersion;
mAccessoryInterface = iface;
mAccessoryConnection = conn;
mControlEndpoint = controlEndpoint;
mTransport = new UsbAccessoryBulkTransport(mLogger, conn, bulkIn, bulkOut);
if (mProtocolVersion >= 2) {
registerHid();
}
startServices();
mTransport.startReading();
return;
}
// Do accessory negotiation.
mLogger.log("Attempting to switch device to accessory mode...");
// Send get protocol.
int protocolVersion = getProtocol(conn);
if (protocolVersion < 1) {
mLogger.logError("Device does not support accessory protocol.");
return;
}
mLogger.log("Protocol version: " + protocolVersion);
// Send identifying strings.
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MANUFACTURER, MANUFACTURER);
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MODEL, MODEL);
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_DESCRIPTION, DESCRIPTION);
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_VERSION, VERSION);
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_URI, URI);
sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_SERIAL, SERIAL);
// Send start.
// The device should re-enumerate as an accessory.
mLogger.log("Sending accessory start request.");
int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_START, 0, 0, null, 0, 10000);
if (len != 0) {
mLogger.logError("Device refused to switch to accessory mode.");
} else {
mLogger.log("Waiting for device to re-enumerate...");
}
} finally {
if (!mConnected) {
conn.releaseInterface(iface);
}
}
}
private void disconnect() {
mLogger.log("Disconnecting from device: " + mDevice);
stopServices();
unregisterHid();
mLogger.log("Disconnected.");
mConnected = false;
mDevice = null;
mAccessoryConnection = null;
mAccessoryInterface = null;
mControlEndpoint = null;
if (mTransport != null) {
mTransport.close();
mTransport = null;
}
}
private void registerHid() {
mLogger.log("Registering HID multitouch device.");
mMultitouch = new UsbHid.Multitouch(MULTITOUCH_REPORT_ID, MULTITOUCH_MAX_CONTACTS,
mSurfaceView.getWidth(), mSurfaceView.getHeight());
mHidBuffer.clear();
mMultitouch.generateDescriptor(mHidBuffer);
mHidBuffer.flip();
mLogger.log("HID descriptor size: " + mHidBuffer.limit());
mLogger.log("HID report size: " + mMultitouch.getReportSize());
final int maxPacketSize = mControlEndpoint.getMaxPacketSize();
mLogger.log("Control endpoint max packet size: " + maxPacketSize);
if (mMultitouch.getReportSize() > maxPacketSize) {
mLogger.logError("HID report is too big for this accessory.");
return;
}
int len = mAccessoryConnection.controlTransfer(
UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_REGISTER_HID,
MULTITOUCH_DEVICE_ID, mHidBuffer.limit(), null, 0, 10000);
if (len != 0) {
mLogger.logError("Device rejected ACCESSORY_REGISTER_HID request.");
return;
}
while (mHidBuffer.hasRemaining()) {
int position = mHidBuffer.position();
int count = Math.min(mHidBuffer.remaining(), maxPacketSize);
len = mAccessoryConnection.controlTransfer(
UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_SET_HID_REPORT_DESC,
MULTITOUCH_DEVICE_ID, 0,
mHidBuffer.array(), position, count, 10000);
if (len != count) {
mLogger.logError("Device rejected ACCESSORY_SET_HID_REPORT_DESC request.");
return;
}
mHidBuffer.position(position + count);
}
mLogger.log("HID device registered.");
mMultitouchEnabled = true;
if (mMultitouchContacts == null) {
mMultitouchContacts = new UsbHid.Multitouch.Contact[MULTITOUCH_MAX_CONTACTS];
for (int i = 0; i < MULTITOUCH_MAX_CONTACTS; i++) {
mMultitouchContacts[i] = new UsbHid.Multitouch.Contact();
}
}
}
private void unregisterHid() {
mMultitouch = null;
mMultitouchContacts = null;
mMultitouchEnabled = false;
}
private void sendHidTouch(MotionEvent event) {
if (mMultitouchEnabled) {
mLogger.log("Sending touch event: " + event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE: {
final int pointerCount =
Math.min(MULTITOUCH_MAX_CONTACTS, event.getPointerCount());
final int historySize = event.getHistorySize();
for (int p = 0; p < pointerCount; p++) {
mMultitouchContacts[p].id = event.getPointerId(p);
}
for (int h = 0; h < historySize; h++) {
for (int p = 0; p < pointerCount; p++) {
mMultitouchContacts[p].x = (int)event.getHistoricalX(p, h);
mMultitouchContacts[p].y = (int)event.getHistoricalY(p, h);
}
sendHidTouchReport(pointerCount);
}
for (int p = 0; p < pointerCount; p++) {
mMultitouchContacts[p].x = (int)event.getX(p);
mMultitouchContacts[p].y = (int)event.getY(p);
}
sendHidTouchReport(pointerCount);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
sendHidTouchReport(0);
break;
}
}
}
private void sendHidTouchReport(int contactCount) {
mHidBuffer.clear();
mMultitouch.generateReport(mHidBuffer, mMultitouchContacts, contactCount);
mHidBuffer.flip();
int count = mHidBuffer.limit();
int len = mAccessoryConnection.controlTransfer(
UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_SEND_HID_EVENT,
MULTITOUCH_DEVICE_ID, 0,
mHidBuffer.array(), 0, count, 10000);
if (len != count) {
mLogger.logError("Device rejected ACCESSORY_SEND_HID_EVENT request.");
return;
}
}
private void startServices() {
mDisplaySinkService = new DisplaySinkService(this, mTransport,
getResources().getConfiguration().densityDpi);
mDisplaySinkService.start();
if (mAttached) {
mDisplaySinkService.setSurfaceView(mSurfaceView);
}
}
private void stopServices() {
if (mDisplaySinkService != null) {
mDisplaySinkService.stop();
mDisplaySinkService = null;
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mAttached = true;
if (mDisplaySinkService != null) {
mDisplaySinkService.setSurfaceView(mSurfaceView);
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
if (mDisplaySinkService != null) {
mDisplaySinkService.setSurfaceView(null);
}
}
private int getProtocol(UsbDeviceConnection conn) {
byte buffer[] = new byte[2];
int len = conn.controlTransfer(
UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_GET_PROTOCOL, 0, 0, buffer, 2, 10000);
if (len != 2) {
return -1;
}
return (buffer[1] << 8) | buffer[0];
}
private void sendString(UsbDeviceConnection conn, int index, String string) {
byte[] buffer = (string + "\0").getBytes();
int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
UsbAccessoryConstants.ACCESSORY_SEND_STRING, 0, index,
buffer, buffer.length, 10000);
if (len != buffer.length) {
mLogger.logError("Failed to send string " + index + ": \"" + string + "\"");
} else {
mLogger.log("Sent string " + index + ": \"" + string + "\"");
}
}
private static boolean isAccessory(UsbDevice device) {
final int vid = device.getVendorId();
final int pid = device.getProductId();
return vid == UsbAccessoryConstants.USB_ACCESSORY_VENDOR_ID
&& (pid == UsbAccessoryConstants.USB_ACCESSORY_PRODUCT_ID
|| pid == UsbAccessoryConstants.USB_ACCESSORY_ADB_PRODUCT_ID);
}
class TextLogger extends Logger {
@Override
public void log(final String message) {
Log.d(TAG, message);
mLogTextView.post(new Runnable() {
@Override
public void run() {
mLogTextView.append(message);
mLogTextView.append("\n");
}
});
}
}
class DeviceReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device != null) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
onDeviceAttached(device);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
onDeviceDetached(device);
} else if (action.equals(ACTION_USB_DEVICE_PERMISSION)) {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
mLogger.log("Device permission granted: " + device);
onDeviceAttached(device);
} else {
mLogger.logError("Device permission denied: " + device);
}
}
}
}
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.accessorydisplay.source" >
<uses-feature android:name="android.hardware.usb.accessory"/>
<uses-sdk android:minSdkVersion="18" />
<application android:label="@string/app_name"
android:icon="@drawable/ic_app"
android:hardwareAccelerated="true">
<activity android:name=".SourceActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
android:resource="@xml/usb_accessory_filter"/>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.opengl.GLSurfaceView android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button android:id="@+id/explode_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:text="Explode!" />
</RelativeLayout>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView android:id="@+id/logTextView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Accessory Display Source</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Match all devices -->
<usb-accessory manufacturer="Android" model="Accessory Display" />
</resources>

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source;
import com.android.accessorydisplay.common.Protocol;
import com.android.accessorydisplay.common.Service;
import com.android.accessorydisplay.common.Transport;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Message;
import android.view.Display;
import android.view.Surface;
import java.nio.ByteBuffer;
public class DisplaySourceService extends Service {
private static final int MSG_DISPATCH_DISPLAY_ADDED = 1;
private static final int MSG_DISPATCH_DISPLAY_REMOVED = 2;
private static final String DISPLAY_NAME = "Accessory Display";
private static final int BIT_RATE = 6000000;
private static final int FRAME_RATE = 30;
private static final int I_FRAME_INTERVAL = 10;
private final Callbacks mCallbacks;
private final ServiceHandler mHandler;
private final DisplayManager mDisplayManager;
private boolean mSinkAvailable;
private int mSinkWidth;
private int mSinkHeight;
private int mSinkDensityDpi;
private VirtualDisplayThread mVirtualDisplayThread;
public DisplaySourceService(Context context, Transport transport, Callbacks callbacks) {
super(context, transport, Protocol.DisplaySourceService.ID);
mCallbacks = callbacks;
mHandler = new ServiceHandler();
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
}
@Override
public void start() {
super.start();
getLogger().log("Sending MSG_QUERY.");
getTransport().sendMessage(Protocol.DisplaySinkService.ID,
Protocol.DisplaySinkService.MSG_QUERY, null);
}
@Override
public void stop() {
super.stop();
handleSinkNotAvailable();
}
@Override
public void onMessageReceived(int service, int what, ByteBuffer content) {
switch (what) {
case Protocol.DisplaySourceService.MSG_SINK_AVAILABLE: {
getLogger().log("Received MSG_SINK_AVAILABLE");
if (content.remaining() >= 12) {
final int width = content.getInt();
final int height = content.getInt();
final int densityDpi = content.getInt();
if (width >= 0 && width <= 4096
&& height >= 0 && height <= 4096
&& densityDpi >= 60 && densityDpi <= 640) {
handleSinkAvailable(width, height, densityDpi);
return;
}
}
getLogger().log("Receive invalid MSG_SINK_AVAILABLE message.");
break;
}
case Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE: {
getLogger().log("Received MSG_SINK_NOT_AVAILABLE");
handleSinkNotAvailable();
break;
}
}
}
private void handleSinkAvailable(int width, int height, int densityDpi) {
if (mSinkAvailable && mSinkWidth == width && mSinkHeight == height
&& mSinkDensityDpi == densityDpi) {
return;
}
getLogger().log("Accessory display sink available: "
+ "width=" + width + ", height=" + height
+ ", densityDpi=" + densityDpi);
mSinkAvailable = true;
mSinkWidth = width;
mSinkHeight = height;
mSinkDensityDpi = densityDpi;
createVirtualDisplay();
}
private void handleSinkNotAvailable() {
getLogger().log("Accessory display sink not available.");
mSinkAvailable = false;
mSinkWidth = 0;
mSinkHeight = 0;
mSinkDensityDpi = 0;
releaseVirtualDisplay();
}
private void createVirtualDisplay() {
releaseVirtualDisplay();
mVirtualDisplayThread = new VirtualDisplayThread(
mSinkWidth, mSinkHeight, mSinkDensityDpi);
mVirtualDisplayThread.start();
}
private void releaseVirtualDisplay() {
if (mVirtualDisplayThread != null) {
mVirtualDisplayThread.quit();
mVirtualDisplayThread = null;
}
}
public interface Callbacks {
public void onDisplayAdded(Display display);
public void onDisplayRemoved(Display display);
}
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DISPATCH_DISPLAY_ADDED: {
mCallbacks.onDisplayAdded((Display)msg.obj);
break;
}
case MSG_DISPATCH_DISPLAY_REMOVED: {
mCallbacks.onDisplayRemoved((Display)msg.obj);
break;
}
}
}
}
private final class VirtualDisplayThread extends Thread {
private static final int TIMEOUT_USEC = 1000000;
private final int mWidth;
private final int mHeight;
private final int mDensityDpi;
private volatile boolean mQuitting;
public VirtualDisplayThread(int width, int height, int densityDpi) {
mWidth = width;
mHeight = height;
mDensityDpi = densityDpi;
}
@Override
public void run() {
MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = codec.createInputSurface();
codec.start();
VirtualDisplay virtualDisplay = mDisplayManager.createPrivateVirtualDisplay(
DISPLAY_NAME, mWidth, mHeight, mDensityDpi, surface);
if (virtualDisplay != null) {
mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_ADDED,
virtualDisplay.getDisplay()).sendToTarget();
stream(codec);
mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_REMOVED,
virtualDisplay.getDisplay()).sendToTarget();
virtualDisplay.release();
}
codec.signalEndOfInputStream();
codec.stop();
}
public void quit() {
mQuitting = true;
}
private void stream(MediaCodec codec) {
BufferInfo info = new BufferInfo();
ByteBuffer[] buffers = null;
while (!mQuitting) {
int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC);
if (index >= 0) {
if (buffers == null) {
buffers = codec.getOutputBuffers();
}
ByteBuffer buffer = buffers[index];
buffer.limit(info.offset + info.size);
buffer.position(info.offset);
getTransport().sendMessage(Protocol.DisplaySinkService.ID,
Protocol.DisplaySinkService.MSG_CONTENT, buffer);
codec.releaseOutputBuffer(index, false);
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
buffers = null;
} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
getLogger().log("Codec dequeue buffer timed out.");
}
}
}
}
}

View File

@@ -0,0 +1,257 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source;
import com.android.accessorydisplay.common.Logger;
import com.android.accessorydisplay.source.presentation.DemoPresentation;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.Display;
import android.widget.TextView;
public class SourceActivity extends Activity {
private static final String TAG = "SourceActivity";
private static final String ACTION_USB_ACCESSORY_PERMISSION =
"com.android.accessorydisplay.source.ACTION_USB_ACCESSORY_PERMISSION";
private static final String MANUFACTURER = "Android";
private static final String MODEL = "Accessory Display";
private UsbManager mUsbManager;
private AccessoryReceiver mReceiver;
private TextView mLogTextView;
private Logger mLogger;
private Presenter mPresenter;
private boolean mConnected;
private UsbAccessory mAccessory;
private UsbAccessoryStreamTransport mTransport;
private DisplaySourceService mDisplaySourceService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE);
setContentView(R.layout.source_activity);
mLogTextView = (TextView) findViewById(R.id.logTextView);
mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance());
mLogger = new TextLogger();
mPresenter = new Presenter();
mLogger.log("Waiting for accessory display sink to be attached to USB...");
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
filter.addAction(ACTION_USB_ACCESSORY_PERMISSION);
mReceiver = new AccessoryReceiver();
registerReceiver(mReceiver, filter);
Intent intent = getIntent();
if (intent.getAction().equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) {
UsbAccessory accessory =
(UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
if (accessory != null) {
onAccessoryAttached(accessory);
}
} else {
UsbAccessory[] accessories = mUsbManager.getAccessoryList();
if (accessories != null) {
for (UsbAccessory accessory : accessories) {
onAccessoryAttached(accessory);
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
@Override
protected void onResume() {
super.onResume();
//new DemoPresentation(this, getWindowManager().getDefaultDisplay()).show();
}
@Override
protected void onPause() {
super.onPause();
}
private void onAccessoryAttached(UsbAccessory accessory) {
mLogger.log("USB accessory attached: " + accessory);
if (!mConnected) {
connect(accessory);
}
}
private void onAccessoryDetached(UsbAccessory accessory) {
mLogger.log("USB accessory detached: " + accessory);
if (mConnected && accessory.equals(mAccessory)) {
disconnect();
}
}
private void connect(UsbAccessory accessory) {
if (!isSink(accessory)) {
mLogger.log("Not connecting to USB accessory because it is not an accessory display sink: "
+ accessory);
return;
}
if (mConnected) {
disconnect();
}
// Check whether we have permission to access the accessory.
if (!mUsbManager.hasPermission(accessory)) {
mLogger.log("Prompting the user for access to the accessory.");
Intent intent = new Intent(ACTION_USB_ACCESSORY_PERMISSION);
intent.setPackage(getPackageName());
PendingIntent pendingIntent = PendingIntent.getBroadcast(
this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
mUsbManager.requestPermission(accessory, pendingIntent);
return;
}
// Open the accessory.
ParcelFileDescriptor fd = mUsbManager.openAccessory(accessory);
if (fd == null) {
mLogger.logError("Could not obtain accessory connection.");
return;
}
// All set.
mLogger.log("Connected.");
mConnected = true;
mAccessory = accessory;
mTransport = new UsbAccessoryStreamTransport(mLogger, fd);
startServices();
mTransport.startReading();
}
private void disconnect() {
mLogger.log("Disconnecting from accessory: " + mAccessory);
stopServices();
mLogger.log("Disconnected.");
mConnected = false;
mAccessory = null;
if (mTransport != null) {
mTransport.close();
mTransport = null;
}
}
private void startServices() {
mDisplaySourceService = new DisplaySourceService(this, mTransport, mPresenter);
mDisplaySourceService.start();
}
private void stopServices() {
if (mDisplaySourceService != null) {
mDisplaySourceService.stop();
mDisplaySourceService = null;
}
}
private static boolean isSink(UsbAccessory accessory) {
return MANUFACTURER.equals(accessory.getManufacturer())
&& MODEL.equals(accessory.getModel());
}
class TextLogger extends Logger {
@Override
public void log(final String message) {
Log.d(TAG, message);
mLogTextView.post(new Runnable() {
@Override
public void run() {
mLogTextView.append(message);
mLogTextView.append("\n");
}
});
}
}
class AccessoryReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
UsbAccessory accessory = intent.<UsbAccessory>getParcelableExtra(
UsbManager.EXTRA_ACCESSORY);
if (accessory != null) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) {
onAccessoryAttached(accessory);
} else if (action.equals(UsbManager.ACTION_USB_ACCESSORY_DETACHED)) {
onAccessoryDetached(accessory);
} else if (action.equals(ACTION_USB_ACCESSORY_PERMISSION)) {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
mLogger.log("Accessory permission granted: " + accessory);
onAccessoryAttached(accessory);
} else {
mLogger.logError("Accessory permission denied: " + accessory);
}
}
}
}
}
class Presenter implements DisplaySourceService.Callbacks {
private DemoPresentation mPresentation;
@Override
public void onDisplayAdded(Display display) {
mLogger.log("Accessory display added: " + display);
mPresentation = new DemoPresentation(SourceActivity.this, display, mLogger);
mPresentation.show();
}
@Override
public void onDisplayRemoved(Display display) {
mLogger.log("Accessory display removed: " + display);
if (mPresentation != null) {
mPresentation.dismiss();
mPresentation = null;
}
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source;
import com.android.accessorydisplay.common.Logger;
import com.android.accessorydisplay.common.Transport;
import android.hardware.usb.UsbAccessory;
import android.os.ParcelFileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Sends or receives messages over a file descriptor associated with a {@link UsbAccessory}.
*/
public class UsbAccessoryStreamTransport extends Transport {
private ParcelFileDescriptor mFd;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
public UsbAccessoryStreamTransport(Logger logger, ParcelFileDescriptor fd) {
super(logger, 16384);
mFd = fd;
mInputStream = new FileInputStream(fd.getFileDescriptor());
mOutputStream = new FileOutputStream(fd.getFileDescriptor());
}
@Override
protected void ioClose() {
try {
mFd.close();
} catch (IOException ex) {
}
mFd = null;
mInputStream = null;
mOutputStream = null;
}
@Override
protected int ioRead(byte[] buffer, int offset, int count) throws IOException {
if (mInputStream == null) {
throw new IOException("Stream was closed.");
}
return mInputStream.read(buffer, offset, count);
}
@Override
protected void ioWrite(byte[] buffer, int offset, int count) throws IOException {
if (mOutputStream == null) {
throw new IOException("Stream was closed.");
}
mOutputStream.write(buffer, offset, count);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source.presentation;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import javax.microedition.khronos.opengles.GL10;
/**
* A vertex shaded cube.
*/
class Cube
{
public Cube()
{
int one = 0x10000;
int vertices[] = {
-one, -one, -one,
one, -one, -one,
one, one, -one,
-one, one, -one,
-one, -one, one,
one, -one, one,
one, one, one,
-one, one, one,
};
int colors[] = {
0, 0, 0, one,
one, 0, 0, one,
one, one, 0, one,
0, one, 0, one,
0, 0, one, one,
one, 0, one, one,
one, one, one, one,
0, one, one, one,
};
byte indices[] = {
0, 4, 5, 0, 5, 1,
1, 5, 6, 1, 6, 2,
2, 6, 7, 2, 7, 3,
3, 7, 4, 3, 4, 0,
4, 7, 6, 4, 6, 5,
3, 0, 1, 3, 1, 2
};
// Buffers to be passed to gl*Pointer() functions
// must be direct, i.e., they must be placed on the
// native heap where the garbage collector cannot
// move them.
//
// Buffers with multi-byte datatypes (e.g., short, int, float)
// must have their byte order set to native order
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
vbb.order(ByteOrder.nativeOrder());
mVertexBuffer = vbb.asIntBuffer();
mVertexBuffer.put(vertices);
mVertexBuffer.position(0);
ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
cbb.order(ByteOrder.nativeOrder());
mColorBuffer = cbb.asIntBuffer();
mColorBuffer.put(colors);
mColorBuffer.position(0);
mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
mIndexBuffer.put(indices);
mIndexBuffer.position(0);
}
public void draw(GL10 gl)
{
gl.glFrontFace(GL10.GL_CW);
gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer);
}
private IntBuffer mVertexBuffer;
private IntBuffer mColorBuffer;
private ByteBuffer mIndexBuffer;
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source.presentation;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView;
/**
* Render a pair of tumbling cubes.
*/
public class CubeRenderer implements GLSurfaceView.Renderer {
private boolean mTranslucentBackground;
private Cube mCube;
private float mAngle;
private float mScale = 1.0f;
private boolean mExploding;
public CubeRenderer(boolean useTranslucentBackground) {
mTranslucentBackground = useTranslucentBackground;
mCube = new Cube();
}
public void explode() {
mExploding = true;
}
public void onDrawFrame(GL10 gl) {
/*
* Usually, the first thing one might want to do is to clear
* the screen. The most efficient way of doing this is to use
* glClear().
*/
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
/*
* Now we're ready to draw some 3D objects
*/
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, -3.0f);
gl.glRotatef(mAngle, 0, 1, 0);
gl.glRotatef(mAngle*0.25f, 1, 0, 0);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glScalef(mScale, mScale, mScale);
mCube.draw(gl);
gl.glRotatef(mAngle*2.0f, 0, 1, 1);
gl.glTranslatef(0.5f, 0.5f, 0.5f);
mCube.draw(gl);
mAngle += 1.2f;
if (mExploding) {
mScale *= 1.02f;
if (mScale > 4.0f) {
mScale = 1.0f;
mExploding = false;
}
}
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
/*
* Set our projection matrix. This doesn't have to be done
* each time we draw, but usually a new projection needs to
* be set when the viewport is resized.
*/
float ratio = (float) width / height;
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
/*
* By default, OpenGL enables features that improve quality
* but reduce performance. One might want to tweak that
* especially on software renderer.
*/
gl.glDisable(GL10.GL_DITHER);
/*
* Some one-time OpenGL initialization can be made here
* probably based on features of this particular context
*/
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
GL10.GL_FASTEST);
if (mTranslucentBackground) {
gl.glClearColor(0,0,0,0);
} else {
gl.glClearColor(1,1,1,1);
}
gl.glEnable(GL10.GL_CULL_FACE);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glEnable(GL10.GL_DEPTH_TEST);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.accessorydisplay.source.presentation;
import com.android.accessorydisplay.common.Logger;
import com.android.accessorydisplay.source.R;
import android.app.Presentation;
import android.content.Context;
import android.content.res.Resources;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
/**
* The presentation to show on the accessory display.
* <p>
* Note that this display may have different metrics from the display on which
* the main activity is showing so we must be careful to use the presentation's
* own {@link Context} whenever we load resources.
* </p>
*/
public final class DemoPresentation extends Presentation {
private final Logger mLogger;
private GLSurfaceView mSurfaceView;
private CubeRenderer mRenderer;
private Button mExplodeButton;
public DemoPresentation(Context context, Display display, Logger logger) {
super(context, display);
mLogger = logger;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Be sure to call the super class.
super.onCreate(savedInstanceState);
// Get the resources for the context of the presentation.
// Notice that we are getting the resources from the context of the presentation.
Resources r = getContext().getResources();
// Inflate the layout.
setContentView(R.layout.presentation_content);
// Set up the surface view for visual interest.
mRenderer = new CubeRenderer(false);
mSurfaceView = (GLSurfaceView)findViewById(R.id.surface_view);
mSurfaceView.setRenderer(mRenderer);
// Add a button.
mExplodeButton = (Button)findViewById(R.id.explode_button);
mExplodeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mRenderer.explode();
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mLogger.log("Received touch event: " + event);
return super.onTouchEvent(event);
}
}