MIDI CTS: added loopback test for MIDI

Install a virtual MIDI devicea and use
it for loopback based CTS tests.

Change-Id: Ia232259aa3d678d43342321e48a260530ff0e6be
Signed-off-by: Phil Burk <philburk@google.com>
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 4035af7b..f2ba287 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -143,6 +143,7 @@
     CtsLocation2TestCases \
     CtsMediaStressTestCases \
     CtsMediaTestCases \
+    CtsMidiTestCases \
     CtsNativeOpenGLTestCases \
     CtsNdefTestCases \
     CtsNetTestCases \
diff --git a/tests/tests/midi/Android.mk b/tests/tests/midi/Android.mk
new file mode 100755
index 0000000..f202933
--- /dev/null
+++ b/tests/tests/midi/Android.mk
@@ -0,0 +1,34 @@
+# Copyright (C) 2015 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 $(CLEAR_VARS)
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := optional
+
+# When built, explicitly put it in the data partition.
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ctsdeviceutil ctstestrunner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Must match the package name in CtsTestCaseList.mk
+LOCAL_PACKAGE_NAME := CtsMidiTestCases
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/tests/midi/AndroidManifest.xml b/tests/tests/midi/AndroidManifest.xml
new file mode 100755
index 0000000..2cdd211
--- /dev/null
+++ b/tests/tests/midi/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2015 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="android.midi.cts">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+
+    <uses-feature android:name="android.software.midi" android:required="true"/>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="MidiEchoTestService">
+            <intent-filter>
+                <action android:name="android.media.midi.MidiDeviceService" />
+            </intent-filter>
+            <meta-data android:name="android.media.midi.MidiDeviceService"
+                android:resource="@xml/echo_device_info" />
+        </service>
+    </application>
+
+    <!--  self-instrumenting test package. -->
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="CTS MIDI tests"
+        android:targetPackage="android.midi.cts" >
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+</manifest>
+
diff --git a/tests/tests/midi/res/xml/echo_device_info.xml b/tests/tests/midi/res/xml/echo_device_info.xml
new file mode 100644
index 0000000..936216a
--- /dev/null
+++ b/tests/tests/midi/res/xml/echo_device_info.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<devices>
+    <device manufacturer="AndroidCTS" product="MidiEcho" tags="echo,test">
+        <input-port name="input" />
+        <output-port name="output" />
+    </device>
+</devices>
diff --git a/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java b/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java
new file mode 100644
index 0000000..f9ef68f
--- /dev/null
+++ b/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2015 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 android.midi.cts;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiOutputPort;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDevice.MidiConnection;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
+import android.os.Bundle;
+import android.test.AndroidTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Random;
+
+/**
+ * Test MIDI using a virtual MIDI device that echos input to output.
+ */
+public class MidiEchoTest extends AndroidTestCase {
+    public static final String TEST_MANUFACTURER = "AndroidCTS";
+    public static final String ECHO_PRODUCT = "MidiEcho";
+    // I am overloading the timestamp for some tests. It is passed
+    // directly through the Echo server unchanged.
+    // The high 32-bits has a recognizable value.
+    // The low 32-bits can contain data used to identify messages.
+    private static final long TIMESTAMP_MARKER = 0x1234567800000000L;
+    private static final long TIMESTAMP_MARKER_MASK = 0xFFFFFFFF00000000L;
+    private static final long TIMESTAMP_DATA_MASK = 0x00000000FFFFFFFFL;
+    private static final long NANOS_PER_MSEC = 1000L * 1000L;
+
+    // Store device and ports related to the Echo service.
+    static class MidiTestContext {
+        MidiDeviceInfo echoInfo;
+        MidiDevice     echoDevice;
+        MidiInputPort  echoInputPort;
+        MidiOutputPort echoOutputPort;
+    }
+
+    // Store complete MIDI message so it can be put in an array.
+    static class MidiMessage {
+        public final byte[] data;
+        public final long   timestamp;
+        public final long   timeReceived;
+
+        MidiMessage(byte[] buffer, int offset, int length, long timestamp) {
+            timeReceived = System.nanoTime();
+            data = new byte[length];
+            System.arraycopy(buffer, offset, data, 0, length);
+            this.timestamp = timestamp;
+        }
+    }
+
+    // Listens for an asynchronous device open and notifies waiting foreground
+    // test.
+    class MyTestOpenCallback implements MidiManager.OnDeviceOpenedListener {
+        MidiDevice mDevice;
+
+        @Override
+        public synchronized void onDeviceOpened(MidiDevice device) {
+            mDevice = device;
+            notifyAll();
+        }
+
+        public synchronized MidiDevice waitForOpen(int msec)
+                throws InterruptedException {
+            wait(msec);
+            return mDevice;
+        }
+    }
+
+    // Store received messages in an array.
+    class MyLoggingReceiver extends MidiReceiver {
+        ArrayList<MidiMessage> messages = new ArrayList<MidiMessage>();
+
+        @Override
+        public synchronized void onSend(byte[] data, int offset, int count,
+                long timestamp) {
+            messages.add(new MidiMessage(data, offset, count, timestamp));
+            notifyAll();
+        }
+
+        public synchronized int getMessageCount() {
+            return messages.size();
+        }
+
+        public synchronized MidiMessage getMessage(int index) {
+            return messages.get(index);
+        }
+
+        /**
+         * Wait until count messages have arrived.
+         * This is a cumulative total.
+         * @param count
+         * @param timeoutMs
+         * @throws InterruptedException
+         */
+        public synchronized void waitForMessages(int count, int timeoutMs)
+                throws InterruptedException {
+            long endTimeMs = System.currentTimeMillis() + timeoutMs + 1;
+            long timeToWait = timeoutMs + 1;
+            while ((getMessageCount() < count)
+                    && (timeToWait > 0)) {
+                wait(timeToWait);
+                timeToWait = endTimeMs - System.currentTimeMillis();
+            }
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    // Search through the available devices for the ECHO loop-back device.
+    protected MidiDeviceInfo findEchoDevice() {
+        MidiManager midiManager = (MidiManager) mContext.getSystemService(
+                Context.MIDI_SERVICE);
+        MidiDeviceInfo[] infos = midiManager.getDevices();
+        MidiDeviceInfo echoInfo = null;
+        for (MidiDeviceInfo info : infos) {
+            Bundle properties = info.getProperties();
+            String manufacturer = (String) properties.get(
+                    MidiDeviceInfo.PROPERTY_MANUFACTURER);
+
+            if (TEST_MANUFACTURER.equals(manufacturer)) {
+                String product = (String) properties.get(
+                        MidiDeviceInfo.PROPERTY_PRODUCT);
+                if (ECHO_PRODUCT.equals(product)) {
+                    echoInfo = info;
+                    break;
+                }
+            }
+        }
+        assertTrue("could not find " + ECHO_PRODUCT, echoInfo != null);
+        return echoInfo;
+    }
+
+    protected MidiTestContext setUpEchoServer() throws Exception {
+        MidiManager midiManager = (MidiManager) mContext.getSystemService(
+                Context.MIDI_SERVICE);
+
+        MidiDeviceInfo echoInfo = findEchoDevice();
+
+        // Open device.
+        MyTestOpenCallback callback = new MyTestOpenCallback();
+        midiManager.openDevice(echoInfo, callback, null);
+        int timeoutMs = 1000;
+        MidiDevice echoDevice = callback.waitForOpen(timeoutMs);
+        assertTrue("could not open " + ECHO_PRODUCT, echoDevice != null);
+
+        // Query echo service directly to see if it is getting status updates.
+        MidiEchoTestService echoService = MidiEchoTestService.getInstance();
+        assertEquals("virtual device status, input port before open", false,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port before open", 0,
+                echoService.outputOpenCount);
+
+        // Open input port.
+        MidiInputPort echoInputPort = echoDevice.openInputPort(0);
+        assertTrue("could not open input port", echoInputPort != null);
+        assertEquals("input port number", 0, echoInputPort.getPortNumber());
+        assertEquals("virtual device status, input port after open", true,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port before open", 0,
+                echoService.outputOpenCount);
+
+        // Open output port.
+        MidiOutputPort echoOutputPort = echoDevice.openOutputPort(0);
+        assertTrue("could not open output port", echoOutputPort != null);
+        assertEquals("output port number", 0, echoOutputPort.getPortNumber());
+        assertEquals("virtual device status, input port after open", true,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port after open", 1,
+                echoService.outputOpenCount);
+
+        MidiTestContext mc = new MidiTestContext();
+        mc.echoInfo = echoInfo;
+        mc.echoDevice = echoDevice;
+        mc.echoInputPort = echoInputPort;
+        mc.echoOutputPort = echoOutputPort;
+        return mc;
+    }
+
+    /**
+     * Close ports and check device status.
+     *
+     * @param mc
+     */
+    protected void tearDownEchoServer(MidiTestContext mc) throws IOException {
+        // Query echo service directly to see if it is getting status updates.
+        MidiEchoTestService echoService = MidiEchoTestService.getInstance();
+        assertEquals("virtual device status, input port before close", true,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port before close", 1,
+                echoService.outputOpenCount);
+
+        // Close output port.
+        mc.echoOutputPort.close();
+        assertEquals("virtual device status, input port before close", true,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port after close", 0,
+                echoService.outputOpenCount);
+        mc.echoOutputPort.close();
+        mc.echoOutputPort.close(); // should be safe to close twice
+
+        // Close input port.
+        mc.echoInputPort.close();
+        assertEquals("virtual device status, input port after close", false,
+                echoService.inputOpened);
+        assertEquals("virtual device status, output port after close", 0,
+                echoService.outputOpenCount);
+        mc.echoInputPort.close();
+        mc.echoInputPort.close(); // should be safe to close twice
+
+        mc.echoDevice.close();
+        mc.echoDevice.close(); // should be safe to close twice
+    }
+
+    /**
+     * @param mc
+     * @param echoInfo
+     */
+    protected void checkEchoDeviceInfo(MidiTestContext mc,
+            MidiDeviceInfo echoInfo) {
+        assertEquals("echo input port count wrong", 1,
+                echoInfo.getInputPortCount());
+        assertEquals("echo output port count wrong", 1,
+                echoInfo.getOutputPortCount());
+
+        Bundle properties = echoInfo.getProperties();
+        String tags = (String) properties.get("tags");
+        assertEquals("attributes from device XML", "echo,test", tags);
+
+        PortInfo[] ports = echoInfo.getPorts();
+        assertEquals("port info array size", 2, ports.length);
+
+        boolean foundInput = false;
+        boolean foundOutput = false;
+        for (PortInfo portInfo : ports) {
+            if (portInfo.getType() == PortInfo.TYPE_INPUT) {
+                foundInput = true;
+                assertEquals("input port name", "input", portInfo.getName());
+
+                assertEquals("info port number", portInfo.getPortNumber(),
+                        mc.echoInputPort.getPortNumber());
+            } else if (portInfo.getType() == PortInfo.TYPE_OUTPUT) {
+                foundOutput = true;
+                assertEquals("output port name", "output", portInfo.getName());
+                assertEquals("info port number", portInfo.getPortNumber(),
+                        mc.echoOutputPort.getPortNumber());
+            }
+        }
+        assertTrue("found input port info", foundInput);
+        assertTrue("found output port info", foundOutput);
+
+        assertEquals("MIDI device type", MidiDeviceInfo.TYPE_VIRTUAL,
+                echoInfo.getType());
+    }
+
+    // Is the MidiManager supported?
+    public void testMidiManager() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+
+        MidiManager midiManager = (MidiManager) mContext.getSystemService(
+                Context.MIDI_SERVICE);
+        assertTrue("MidiManager not supported.", midiManager != null);
+
+        // There should be at least one device for the Echo server.
+        MidiDeviceInfo[] infos = midiManager.getDevices();
+        assertTrue("device list was null", infos != null);
+        assertTrue("device list was empty", infos.length >= 1);
+    }
+
+    public void testDeviceInfo() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+
+        MidiTestContext mc = setUpEchoServer();
+        checkEchoDeviceInfo(mc, mc.echoInfo);
+        checkEchoDeviceInfo(mc, mc.echoDevice.getInfo());
+        assertTrue("device info equal",
+                mc.echoInfo.equals(mc.echoDevice.getInfo()));
+        tearDownEchoServer(mc);
+    }
+
+    public void testEchoSmallMessage() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+
+        MidiTestContext mc = setUpEchoServer();
+
+        MyLoggingReceiver receiver = new MyLoggingReceiver();
+        mc.echoOutputPort.connect(receiver);
+
+        final byte[] buffer = { (byte) 0x93, 0x47, 0x52 };
+        long timestamp = 0x0123765489ABFEDCL;
+
+        mc.echoInputPort.send(buffer, 0, 0, timestamp); // should be a NOOP
+        mc.echoInputPort.send(buffer, 0, buffer.length, timestamp);
+        mc.echoInputPort.send(buffer, 0, 0, timestamp); // should be a NOOP
+
+        // Wait for message to pass quickly through echo service.
+        final int numMessages = 1;
+        final int timeoutMs = 20;
+        synchronized (receiver) {
+            receiver.waitForMessages(numMessages, timeoutMs);
+        }
+        assertEquals("number of messages.", numMessages, receiver.getMessageCount());
+        MidiMessage message = receiver.getMessage(0);
+
+        assertEquals("byte count of message", buffer.length,
+                message.data.length);
+        assertEquals("timestamp in message", timestamp, message.timestamp);
+        for (int i = 0; i < buffer.length; i++) {
+            assertEquals("message byte[" + i + "]", buffer[i] & 0x0FF,
+                    message.data[i] & 0x0FF);
+        }
+
+        mc.echoOutputPort.disconnect(receiver);
+        tearDownEchoServer(mc);
+    }
+
+    public void testEchoLatency() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+
+        MidiTestContext mc = setUpEchoServer();
+        MyLoggingReceiver receiver = new MyLoggingReceiver();
+        mc.echoOutputPort.connect(receiver);
+
+        final int numMessages = 10;
+        final long maxLatencyNanos = 15 * NANOS_PER_MSEC; // generally < 3 msec on N6
+        byte[] buffer = { (byte) 0x93, 0, 64 };
+
+        // Send multiple messages in a burst.
+        for (int index = 0; index < numMessages; index++) {
+            buffer[1] = (byte) (60 + index);
+            mc.echoInputPort.send(buffer, 0, buffer.length, System.nanoTime());
+        }
+
+        // Wait for messages to pass quickly through echo service.
+        final int timeoutMs = 100;
+        synchronized (receiver) {
+            receiver.waitForMessages(numMessages, timeoutMs);
+        }
+        assertEquals("number of messages.", numMessages, receiver.getMessageCount());
+
+        for (int index = 0; index < numMessages; index++) {
+            MidiMessage message = receiver.getMessage(index);
+            assertEquals("message index", (byte) (60 + index), message.data[1]);
+            long elapsedNanos = message.timeReceived - message.timestamp;
+            // If this test fails then there may be a problem with the thread scheduler
+            // or there may be kernel activity that is blocking execution at the user level.
+            assertTrue("MIDI round trip latency[" + index + "] too large, " + elapsedNanos + " nanoseconds",
+                    (elapsedNanos < maxLatencyNanos));
+        }
+
+        mc.echoOutputPort.disconnect(receiver);
+        tearDownEchoServer(mc);
+    }
+
+    public void testEchoMultipleMessages() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+
+        MidiTestContext mc = setUpEchoServer();
+
+        MyLoggingReceiver receiver = new MyLoggingReceiver();
+        mc.echoOutputPort.connect(receiver);
+
+        final byte[] buffer = new byte[2048];
+
+        final int numMessages = 100;
+        Random random = new Random(1972941337);
+        int bytesSent = 0;
+        byte value = 0;
+
+        // Send various length messages with sequential bytes.
+        long timestamp = TIMESTAMP_MARKER;
+        for (int messageIndex = 0; messageIndex < numMessages;
+                messageIndex++) {
+            // Sweep numData across critical region of
+            // MidiPortImpl.MAX_PACKET_DATA_SIZE
+            int numData = 1000 + messageIndex;
+            for (int dataIndex = 0; dataIndex < numData; dataIndex++) {
+                buffer[dataIndex] = value;
+                value++;
+            }
+            // This may get split into multiple sends internally.
+            mc.echoInputPort.send(buffer, 0, numData, timestamp);
+            bytesSent += numData;
+            timestamp++;
+        }
+
+        // Check messages. Data must be sequential bytes.
+        value = 0;
+        int bytesReceived = 0;
+        int messageReceivedIndex = 0;
+        int messageSentIndex = 0;
+        int expectedMessageSentIndex = 0;
+        while (bytesReceived < bytesSent) {
+            final int timeoutMs = 500;
+            // Wait for next message.
+            synchronized (receiver) {
+                receiver.waitForMessages(messageReceivedIndex + 1, timeoutMs);
+            }
+            MidiMessage message = receiver.getMessage(messageReceivedIndex++);
+            // parse timestamp marker and data
+            long timestampMarker = message.timestamp & TIMESTAMP_MARKER_MASK;
+            assertEquals("timestamp marker corrupted", TIMESTAMP_MARKER, timestampMarker);
+            messageSentIndex = (int) (message.timestamp & TIMESTAMP_DATA_MASK);
+
+            int numData = message.data.length;
+            for (int dataIndex = 0; dataIndex < numData; dataIndex++) {
+                String msg = String.format("message[%d/%d].data[%d/%d]",
+                        messageReceivedIndex, messageSentIndex, dataIndex,
+                        numData);
+                assertEquals(msg, value, message.data[dataIndex]);
+                value++;
+            }
+            bytesReceived += numData;
+            // May not advance if message got split
+            if (messageSentIndex > expectedMessageSentIndex) {
+                expectedMessageSentIndex++; // only advance by one each message
+            }
+            assertEquals("timestamp in message", expectedMessageSentIndex,
+                    messageSentIndex);
+        }
+
+        mc.echoOutputPort.disconnect(receiver);
+        tearDownEchoServer(mc);
+    }
+
+    // What happens if the app does bad things.
+    public void testEchoBadBehavior() throws Exception {
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+            return; // Not supported so don't test it.
+        }
+        MidiTestContext mc = setUpEchoServer();
+
+        // This should fail because it is already open.
+        MidiInputPort echoInputPort2 = mc.echoDevice.openInputPort(0);
+        assertTrue("input port opened twice", echoInputPort2 == null);
+
+        tearDownEchoServer(mc);
+    }
+}
diff --git a/tests/tests/midi/src/android/midi/cts/MidiEchoTestService.java b/tests/tests/midi/src/android/midi/cts/MidiEchoTestService.java
new file mode 100644
index 0000000..ae5373e
--- /dev/null
+++ b/tests/tests/midi/src/android/midi/cts/MidiEchoTestService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2015 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 android.midi.cts;
+
+import android.media.midi.MidiDeviceService;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Virtual MIDI Device that copies its input to its output.
+ * This is used for loop-back testing of MIDI I/O.
+ */
+
+public class MidiEchoTestService extends MidiDeviceService {
+
+    // Other apps will write to this port.
+    private MidiReceiver mInputReceiver = new MyReceiver();
+    // This app will copy the data to this port.
+    private MidiReceiver mOutputReceiver;
+    private static MidiEchoTestService mInstance;
+
+    // These are public so we can easily read them from CTS test.
+    public int statusChangeCount;
+    public boolean inputOpened;
+    public int outputOpenCount;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mInstance = this;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+    // For CTS testing, so I can read test fields.
+    public static MidiEchoTestService getInstance() {
+        return mInstance;
+    }
+
+    @Override
+    public MidiReceiver[] onGetInputPortReceivers() {
+        return new MidiReceiver[] { mInputReceiver };
+    }
+
+    class MyReceiver extends MidiReceiver {
+        @Override
+        public void onSend(byte[] data, int offset, int count, long timestamp)
+                throws IOException {
+            if (mOutputReceiver == null) {
+                mOutputReceiver = getOutputPortReceivers()[0];
+            }
+            // Copy input to output.
+            mOutputReceiver.send(data, offset, count, timestamp);
+        }
+    }
+
+    @Override
+    public void onDeviceStatusChanged(MidiDeviceStatus status) {
+        statusChangeCount++;
+        inputOpened = status.isInputPortOpen(0);
+        outputOpenCount = status.getOutputPortOpenCount(0);
+    }
+}