| /* |
| * 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); |
| } |
| } |