blob: f9b6d01006cd771bab844339f1435933b762e391 [file] [log] [blame]
/*
* 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.MidiDevice;
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.MidiManager;
import android.media.midi.MidiOutputPort;
import android.media.midi.MidiReceiver;
import android.os.Bundle;
import android.test.AndroidTestCase;
import android.util.Log;
import com.android.midi.CTSMidiEchoTestService;
import com.android.midi.MidiEchoTestService;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;
/**
* Test MIDI using a virtual MIDI device that echos input to output.
*/
public class MidiEchoTest extends AndroidTestCase {
private static final String TAG = "MidiEchoTest";
private static final boolean DEBUG = false;
// 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;
// On a fast device in 2016, the test fails if timeout is 3 but works if it is 4.
// So this timeout value is very generous.
private static final int TIMEOUT_OPEN_MSEC = 1000; // arbitrary
// On a fast device in 2016, the test fails if timeout is 0 but works if it is 1.
// So this timeout value is very generous.
private static final int TIMEOUT_STATUS_MSEC = 500; // arbitrary
// This is defined in MidiPortImpl.java as the maximum payload that
// can be sent internally by MidiInputPort in a
// SOCK_SEQPACKET datagram.
private static final int MAX_PACKET_DATA_SIZE = 1024 - 9;
// 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 {
long deadline = System.currentTimeMillis() + msec;
long timeRemaining = msec;
while (mDevice == null && timeRemaining > 0) {
wait(timeRemaining);
timeRemaining = deadline - System.currentTimeMillis();
}
return mDevice;
}
}
// Store received messages in an array.
class MyLoggingReceiver extends MidiReceiver {
ArrayList<MidiMessage> messages = new ArrayList<MidiMessage>();
int mByteCount;
@Override
public synchronized void onSend(byte[] data, int offset, int count,
long timestamp) {
messages.add(new MidiMessage(data, offset, count, timestamp));
mByteCount += count;
notifyAll();
}
public synchronized int getMessageCount() {
return messages.size();
}
public synchronized int getByteCount() {
return mByteCount;
}
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();
}
}
/**
* Wait until count bytes have arrived. This is a cumulative total.
*
* @param count
* @param timeoutMs
* @throws InterruptedException
*/
public synchronized void waitForBytes(int count, int timeoutMs)
throws InterruptedException {
long endTimeMs = System.currentTimeMillis() + timeoutMs + 1;
long timeToWait = timeoutMs + 1;
while ((getByteCount() < 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();
}
protected MidiTestContext setUpEchoServer() throws Exception {
if (DEBUG) {
Log.i(TAG, "setUpEchoServer()");
}
MidiManager midiManager = (MidiManager) mContext.getSystemService(
Context.MIDI_SERVICE);
MidiDeviceInfo echoInfo = CTSMidiEchoTestService.findEchoDevice(mContext);
// Open device.
MyTestOpenCallback callback = new MyTestOpenCallback();
midiManager.openDevice(echoInfo, callback, null);
MidiDevice echoDevice = callback.waitForOpen(TIMEOUT_OPEN_MSEC);
assertTrue("could not open "
+ CTSMidiEchoTestService.getEchoServerName(), echoDevice != null);
// Query echo service directly to see if it is getting status updates.
MidiEchoTestService echoService = CTSMidiEchoTestService.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 = CTSMidiEchoTestService.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());
assertEquals("MIDI default protocol", MidiDeviceInfo.PROTOCOL_UNKNOWN,
echoInfo.getDefaultProtocol());
}
// 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);
Collection<MidiDeviceInfo> legacyDeviceInfos = midiManager.getDevicesForTransport(
MidiManager.TRANSPORT_MIDI_BYTE_STREAM);
assertTrue("Legacy Device list was null.", legacyDeviceInfos != null);
assertTrue("Legacy Device list was empty", legacyDeviceInfos.size() >= 1);
Collection<MidiDeviceInfo> universalDeviceInfos = midiManager.getDevicesForTransport(
MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS);
assertTrue("Universal Device list was null.", universalDeviceInfos != null);
}
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 {
checkEchoVariableMessage(3);
}
public void testEchoLargeMessage() throws Exception {
checkEchoVariableMessage(MAX_PACKET_DATA_SIZE);
}
// This message will not fit in the internal buffer in MidiInputPort.
// But it is still a legal size according to the API for
// MidiReceiver.send(). It may be received in multiple packets.
public void testEchoOversizeMessage() throws Exception {
checkEchoVariableMessage(MAX_PACKET_DATA_SIZE + 20);
}
// Send a variable sized message. The actual
// size will be a multiple of 3 because it sends NoteOns.
public void checkEchoVariableMessage(int messageSize) 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);
// Send an integral number of notes
int numNotes = messageSize / 3;
int noteSize = numNotes * 3;
final byte[] buffer = new byte[noteSize];
int index = 0;
for (int i = 0; i < numNotes; i++) {
buffer[index++] = (byte) (0x90 + (i & 0x0F)); // NoteOn
buffer[index++] = (byte) 0x47; // Pitch
buffer[index++] = (byte) 0x52; // Velocity
};
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.
// Message sent may have been split into multiple received messages.
// So wait until we receive all the expected bytes.
final int numBytesExpected = buffer.length;
final int timeoutMs = 20;
synchronized (receiver) {
receiver.waitForBytes(numBytesExpected, timeoutMs);
}
// Check total size.
final int numReceived = receiver.getMessageCount();
int totalBytesReceived = 0;
for (int i = 0; i < numReceived; i++) {
MidiMessage message = receiver.getMessage(i);
totalBytesReceived += message.data.length;
assertEquals("timestamp in message", timestamp, message.timestamp);
}
assertEquals("byte count of messages", numBytesExpected,
totalBytesReceived);
// Make sure the payload was not corrupted.
int sentIndex = 0;
for (int i = 0; i < numReceived; i++) {
MidiMessage message = receiver.getMessage(i);
for (int k = 0; k < message.data.length; k++) {
assertEquals("message byte[" + i + "]",
buffer[sentIndex++] & 0x0FF,
message.data[k] & 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 int maxLatencyMs = 15; // generally < 3 msec on N6
final long maxLatencyNanos = maxLatencyMs * NANOS_PER_MSEC;
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 = (numMessages * maxLatencyMs) + 20;
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);
}
// Store history of status changes.
private class MyDeviceCallback extends MidiManager.DeviceCallback {
private volatile MidiDeviceStatus mStatus;
private MidiDeviceInfo mInfo;
public MyDeviceCallback(MidiDeviceInfo info) {
mInfo = info;
}
@Override
public synchronized void onDeviceStatusChanged(MidiDeviceStatus status) {
super.onDeviceStatusChanged(status);
// Filter out status reports from unrelated devices.
if (mInfo.equals(status.getDeviceInfo())) {
mStatus = status;
notifyAll();
}
}
// Wait for a timeout or a notify().
// Return status message or a null if it times out.
public synchronized MidiDeviceStatus waitForStatus(int msec)
throws InterruptedException {
long deadline = System.currentTimeMillis() + msec;
long timeRemaining = msec;
while (mStatus == null && timeRemaining > 0) {
wait(timeRemaining);
timeRemaining = deadline - System.currentTimeMillis();
}
return mStatus;
}
public synchronized void clear() {
mStatus = null;
}
}
// Test callback for onDeviceStatusChanged().
public void testDeviceCallback() 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);
MidiDeviceInfo echoInfo = CTSMidiEchoTestService.findEchoDevice(mContext);
// Open device.
MyTestOpenCallback callback = new MyTestOpenCallback();
midiManager.openDevice(echoInfo, callback, null);
MidiDevice echoDevice = callback.waitForOpen(TIMEOUT_OPEN_MSEC);
assertTrue("could not open " + CTSMidiEchoTestService.getEchoServerName(), echoDevice != null);
MyDeviceCallback deviceCallback = new MyDeviceCallback(echoInfo);
try {
midiManager.registerDeviceCallback(deviceCallback, null);
MidiDeviceStatus status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC);
// The DeviceStatus callback is supposed to be "sticky".
// That means we expect to get the status of every device that is
// already available when we register for the callback.
// If it was not "sticky" then we would only get a callback when there
// was a change in the available devices.
// TODO Often this is null. But sometimes not. Why?
if (status == null) {
Log.d(TAG, "testDeviceCallback() first status was null!");
} else {
// InputPort should be closed because we have not opened it yet.
assertEquals("input port should be closed before we open it.",
false, status.isInputPortOpen(0));
}
// Open input port.
MidiInputPort echoInputPort = echoDevice.openInputPort(0);
assertTrue("could not open input port", echoInputPort != null);
status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC);
assertTrue("should have status by now", null != status);
assertEquals("input port should be open", true, status.isInputPortOpen(0));
deviceCallback.clear();
echoInputPort.close();
status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC);
assertTrue("should have status by now", null != status);
assertEquals("input port should be closed", false, status.isInputPortOpen(0));
// Make sure we do NOT get called after unregistering.
midiManager.unregisterDeviceCallback(deviceCallback);
deviceCallback.clear();
echoInputPort = echoDevice.openInputPort(0);
assertTrue("could not open input port", echoInputPort != null);
status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC);
assertEquals("should not get status after unregistering", null, status);
echoInputPort.close();
} finally {
// Safe to call twice.
midiManager.unregisterDeviceCallback(deviceCallback);
echoDevice.close();
}
}
}