1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.midi.cts; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.media.midi.MidiManager; 22 import android.media.midi.MidiOutputPort; 23 import android.media.midi.MidiDevice; 24 import android.media.midi.MidiDevice.MidiConnection; 25 import android.media.midi.MidiDeviceInfo; 26 import android.media.midi.MidiDeviceInfo.PortInfo; 27 import android.media.midi.MidiDeviceStatus; 28 import android.media.midi.MidiInputPort; 29 import android.media.midi.MidiReceiver; 30 import android.media.midi.MidiSender; 31 import android.os.Bundle; 32 import android.test.AndroidTestCase; 33 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.Random; 37 38 /** 39 * Test MIDI using a virtual MIDI device that echos input to output. 40 */ 41 public class MidiEchoTest extends AndroidTestCase { 42 public static final String TEST_MANUFACTURER = "AndroidCTS"; 43 public static final String ECHO_PRODUCT = "MidiEcho"; 44 // I am overloading the timestamp for some tests. It is passed 45 // directly through the Echo server unchanged. 46 // The high 32-bits has a recognizable value. 47 // The low 32-bits can contain data used to identify messages. 48 private static final long TIMESTAMP_MARKER = 0x1234567800000000L; 49 private static final long TIMESTAMP_MARKER_MASK = 0xFFFFFFFF00000000L; 50 private static final long TIMESTAMP_DATA_MASK = 0x00000000FFFFFFFFL; 51 private static final long NANOS_PER_MSEC = 1000L * 1000L; 52 53 // On a fast device in 2016, the test fails if timeout is 3 but works if it is 4. 54 // So this timeout value is very generous. 55 private static final int TIMEOUT_OPEN_MSEC = 1000; // arbitrary 56 // On a fast device in 2016, the test fails if timeout is 0 but works if it is 1. 57 // So this timeout value is very generous. 58 private static final int TIMEOUT_STATUS_MSEC = 500; // arbitrary 59 60 // Store device and ports related to the Echo service. 61 static class MidiTestContext { 62 MidiDeviceInfo echoInfo; 63 MidiDevice echoDevice; 64 MidiInputPort echoInputPort; 65 MidiOutputPort echoOutputPort; 66 } 67 68 // Store complete MIDI message so it can be put in an array. 69 static class MidiMessage { 70 public final byte[] data; 71 public final long timestamp; 72 public final long timeReceived; 73 74 MidiMessage(byte[] buffer, int offset, int length, long timestamp) { 75 timeReceived = System.nanoTime(); 76 data = new byte[length]; 77 System.arraycopy(buffer, offset, data, 0, length); 78 this.timestamp = timestamp; 79 } 80 } 81 82 // Listens for an asynchronous device open and notifies waiting foreground 83 // test. 84 class MyTestOpenCallback implements MidiManager.OnDeviceOpenedListener { 85 MidiDevice mDevice; 86 87 @Override 88 public synchronized void onDeviceOpened(MidiDevice device) { 89 mDevice = device; 90 notifyAll(); 91 } 92 93 public synchronized MidiDevice waitForOpen(int msec) 94 throws InterruptedException { 95 long deadline = System.currentTimeMillis() + msec; 96 long timeRemaining = msec; 97 while (mDevice == null && timeRemaining > 0) { 98 wait(timeRemaining); 99 timeRemaining = deadline - System.currentTimeMillis(); 100 } 101 return mDevice; 102 } 103 } 104 105 // Store received messages in an array. 106 class MyLoggingReceiver extends MidiReceiver { 107 ArrayList<MidiMessage> messages = new ArrayList<MidiMessage>(); 108 109 @Override 110 public synchronized void onSend(byte[] data, int offset, int count, 111 long timestamp) { 112 messages.add(new MidiMessage(data, offset, count, timestamp)); 113 notifyAll(); 114 } 115 116 public synchronized int getMessageCount() { 117 return messages.size(); 118 } 119 120 public synchronized MidiMessage getMessage(int index) { 121 return messages.get(index); 122 } 123 124 /** 125 * Wait until count messages have arrived. This is a cumulative total. 126 * 127 * @param count 128 * @param timeoutMs 129 * @throws InterruptedException 130 */ 131 public synchronized void waitForMessages(int count, int timeoutMs) 132 throws InterruptedException { 133 long endTimeMs = System.currentTimeMillis() + timeoutMs + 1; 134 long timeToWait = timeoutMs + 1; 135 while ((getMessageCount() < count) 136 && (timeToWait > 0)) { 137 wait(timeToWait); 138 timeToWait = endTimeMs - System.currentTimeMillis(); 139 } 140 } 141 } 142 143 @Override 144 protected void setUp() throws Exception { 145 super.setUp(); 146 } 147 148 @Override 149 protected void tearDown() throws Exception { 150 super.tearDown(); 151 } 152 153 // Search through the available devices for the ECHO loop-back device. 154 protected MidiDeviceInfo findEchoDevice() { 155 MidiManager midiManager = (MidiManager) mContext.getSystemService( 156 Context.MIDI_SERVICE); 157 MidiDeviceInfo[] infos = midiManager.getDevices(); 158 MidiDeviceInfo echoInfo = null; 159 for (MidiDeviceInfo info : infos) { 160 Bundle properties = info.getProperties(); 161 String manufacturer = (String) properties.get( 162 MidiDeviceInfo.PROPERTY_MANUFACTURER); 163 164 if (TEST_MANUFACTURER.equals(manufacturer)) { 165 String product = (String) properties.get( 166 MidiDeviceInfo.PROPERTY_PRODUCT); 167 if (ECHO_PRODUCT.equals(product)) { 168 echoInfo = info; 169 break; 170 } 171 } 172 } 173 assertTrue("could not find " + ECHO_PRODUCT, echoInfo != null); 174 return echoInfo; 175 } 176 177 protected MidiTestContext setUpEchoServer() throws Exception { 178 MidiManager midiManager = (MidiManager) mContext.getSystemService( 179 Context.MIDI_SERVICE); 180 181 MidiDeviceInfo echoInfo = findEchoDevice(); 182 183 // Open device. 184 MyTestOpenCallback callback = new MyTestOpenCallback(); 185 midiManager.openDevice(echoInfo, callback, null); 186 MidiDevice echoDevice = callback.waitForOpen(TIMEOUT_OPEN_MSEC); 187 assertTrue("could not open " + ECHO_PRODUCT, echoDevice != null); 188 189 // Query echo service directly to see if it is getting status updates. 190 MidiEchoTestService echoService = MidiEchoTestService.getInstance(); 191 assertEquals("virtual device status, input port before open", false, 192 echoService.inputOpened); 193 assertEquals("virtual device status, output port before open", 0, 194 echoService.outputOpenCount); 195 196 // Open input port. 197 MidiInputPort echoInputPort = echoDevice.openInputPort(0); 198 assertTrue("could not open input port", echoInputPort != null); 199 assertEquals("input port number", 0, echoInputPort.getPortNumber()); 200 assertEquals("virtual device status, input port after open", true, 201 echoService.inputOpened); 202 assertEquals("virtual device status, output port before open", 0, 203 echoService.outputOpenCount); 204 205 // Open output port. 206 MidiOutputPort echoOutputPort = echoDevice.openOutputPort(0); 207 assertTrue("could not open output port", echoOutputPort != null); 208 assertEquals("output port number", 0, echoOutputPort.getPortNumber()); 209 assertEquals("virtual device status, input port after open", true, 210 echoService.inputOpened); 211 assertEquals("virtual device status, output port after open", 1, 212 echoService.outputOpenCount); 213 214 MidiTestContext mc = new MidiTestContext(); 215 mc.echoInfo = echoInfo; 216 mc.echoDevice = echoDevice; 217 mc.echoInputPort = echoInputPort; 218 mc.echoOutputPort = echoOutputPort; 219 return mc; 220 } 221 222 /** 223 * Close ports and check device status. 224 * 225 * @param mc 226 */ 227 protected void tearDownEchoServer(MidiTestContext mc) throws IOException { 228 // Query echo service directly to see if it is getting status updates. 229 MidiEchoTestService echoService = MidiEchoTestService.getInstance(); 230 assertEquals("virtual device status, input port before close", true, 231 echoService.inputOpened); 232 assertEquals("virtual device status, output port before close", 1, 233 echoService.outputOpenCount); 234 235 // Close output port. 236 mc.echoOutputPort.close(); 237 assertEquals("virtual device status, input port before close", true, 238 echoService.inputOpened); 239 assertEquals("virtual device status, output port after close", 0, 240 echoService.outputOpenCount); 241 mc.echoOutputPort.close(); 242 mc.echoOutputPort.close(); // should be safe to close twice 243 244 // Close input port. 245 mc.echoInputPort.close(); 246 assertEquals("virtual device status, input port after close", false, 247 echoService.inputOpened); 248 assertEquals("virtual device status, output port after close", 0, 249 echoService.outputOpenCount); 250 mc.echoInputPort.close(); 251 mc.echoInputPort.close(); // should be safe to close twice 252 253 mc.echoDevice.close(); 254 mc.echoDevice.close(); // should be safe to close twice 255 } 256 257 /** 258 * @param mc 259 * @param echoInfo 260 */ 261 protected void checkEchoDeviceInfo(MidiTestContext mc, 262 MidiDeviceInfo echoInfo) { 263 assertEquals("echo input port count wrong", 1, 264 echoInfo.getInputPortCount()); 265 assertEquals("echo output port count wrong", 1, 266 echoInfo.getOutputPortCount()); 267 268 Bundle properties = echoInfo.getProperties(); 269 String tags = (String) properties.get("tags"); 270 assertEquals("attributes from device XML", "echo,test", tags); 271 272 PortInfo[] ports = echoInfo.getPorts(); 273 assertEquals("port info array size", 2, ports.length); 274 275 boolean foundInput = false; 276 boolean foundOutput = false; 277 for (PortInfo portInfo : ports) { 278 if (portInfo.getType() == PortInfo.TYPE_INPUT) { 279 foundInput = true; 280 assertEquals("input port name", "input", portInfo.getName()); 281 282 assertEquals("info port number", portInfo.getPortNumber(), 283 mc.echoInputPort.getPortNumber()); 284 } else if (portInfo.getType() == PortInfo.TYPE_OUTPUT) { 285 foundOutput = true; 286 assertEquals("output port name", "output", portInfo.getName()); 287 assertEquals("info port number", portInfo.getPortNumber(), 288 mc.echoOutputPort.getPortNumber()); 289 } 290 } 291 assertTrue("found input port info", foundInput); 292 assertTrue("found output port info", foundOutput); 293 294 assertEquals("MIDI device type", MidiDeviceInfo.TYPE_VIRTUAL, 295 echoInfo.getType()); 296 } 297 298 // Is the MidiManager supported? 299 public void testMidiManager() throws Exception { 300 PackageManager pm = mContext.getPackageManager(); 301 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 302 return; // Not supported so don't test it. 303 } 304 305 MidiManager midiManager = (MidiManager) mContext.getSystemService( 306 Context.MIDI_SERVICE); 307 assertTrue("MidiManager not supported.", midiManager != null); 308 309 // There should be at least one device for the Echo server. 310 MidiDeviceInfo[] infos = midiManager.getDevices(); 311 assertTrue("device list was null", infos != null); 312 assertTrue("device list was empty", infos.length >= 1); 313 } 314 315 public void testDeviceInfo() throws Exception { 316 PackageManager pm = mContext.getPackageManager(); 317 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 318 return; // Not supported so don't test it. 319 } 320 321 MidiTestContext mc = setUpEchoServer(); 322 checkEchoDeviceInfo(mc, mc.echoInfo); 323 checkEchoDeviceInfo(mc, mc.echoDevice.getInfo()); 324 assertTrue("device info equal", 325 mc.echoInfo.equals(mc.echoDevice.getInfo())); 326 tearDownEchoServer(mc); 327 } 328 329 public void testEchoSmallMessage() throws Exception { 330 PackageManager pm = mContext.getPackageManager(); 331 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 332 return; // Not supported so don't test it. 333 } 334 335 MidiTestContext mc = setUpEchoServer(); 336 337 MyLoggingReceiver receiver = new MyLoggingReceiver(); 338 mc.echoOutputPort.connect(receiver); 339 340 final byte[] buffer = { 341 (byte) 0x93, 0x47, 0x52 342 }; 343 long timestamp = 0x0123765489ABFEDCL; 344 345 mc.echoInputPort.send(buffer, 0, 0, timestamp); // should be a NOOP 346 mc.echoInputPort.send(buffer, 0, buffer.length, timestamp); 347 mc.echoInputPort.send(buffer, 0, 0, timestamp); // should be a NOOP 348 349 // Wait for message to pass quickly through echo service. 350 final int numMessages = 1; 351 final int timeoutMs = 20; 352 synchronized (receiver) { 353 receiver.waitForMessages(numMessages, timeoutMs); 354 } 355 assertEquals("number of messages.", numMessages, receiver.getMessageCount()); 356 MidiMessage message = receiver.getMessage(0); 357 358 assertEquals("byte count of message", buffer.length, 359 message.data.length); 360 assertEquals("timestamp in message", timestamp, message.timestamp); 361 for (int i = 0; i < buffer.length; i++) { 362 assertEquals("message byte[" + i + "]", buffer[i] & 0x0FF, 363 message.data[i] & 0x0FF); 364 } 365 366 mc.echoOutputPort.disconnect(receiver); 367 tearDownEchoServer(mc); 368 } 369 370 public void testEchoLatency() throws Exception { 371 PackageManager pm = mContext.getPackageManager(); 372 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 373 return; // Not supported so don't test it. 374 } 375 376 MidiTestContext mc = setUpEchoServer(); 377 MyLoggingReceiver receiver = new MyLoggingReceiver(); 378 mc.echoOutputPort.connect(receiver); 379 380 final int numMessages = 10; 381 final long maxLatencyNanos = 15 * NANOS_PER_MSEC; // generally < 3 msec on N6 382 byte[] buffer = { 383 (byte) 0x93, 0, 64 384 }; 385 386 // Send multiple messages in a burst. 387 for (int index = 0; index < numMessages; index++) { 388 buffer[1] = (byte) (60 + index); 389 mc.echoInputPort.send(buffer, 0, buffer.length, System.nanoTime()); 390 } 391 392 // Wait for messages to pass quickly through echo service. 393 final int timeoutMs = 100; 394 synchronized (receiver) { 395 receiver.waitForMessages(numMessages, timeoutMs); 396 } 397 assertEquals("number of messages.", numMessages, receiver.getMessageCount()); 398 399 for (int index = 0; index < numMessages; index++) { 400 MidiMessage message = receiver.getMessage(index); 401 assertEquals("message index", (byte) (60 + index), message.data[1]); 402 long elapsedNanos = message.timeReceived - message.timestamp; 403 // If this test fails then there may be a problem with the thread scheduler 404 // or there may be kernel activity that is blocking execution at the user level. 405 assertTrue("MIDI round trip latency[" + index + "] too large, " + elapsedNanos 406 + " nanoseconds", 407 (elapsedNanos < maxLatencyNanos)); 408 } 409 410 mc.echoOutputPort.disconnect(receiver); 411 tearDownEchoServer(mc); 412 } 413 414 public void testEchoMultipleMessages() throws Exception { 415 PackageManager pm = mContext.getPackageManager(); 416 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 417 return; // Not supported so don't test it. 418 } 419 420 MidiTestContext mc = setUpEchoServer(); 421 422 MyLoggingReceiver receiver = new MyLoggingReceiver(); 423 mc.echoOutputPort.connect(receiver); 424 425 final byte[] buffer = new byte[2048]; 426 427 final int numMessages = 100; 428 Random random = new Random(1972941337); 429 int bytesSent = 0; 430 byte value = 0; 431 432 // Send various length messages with sequential bytes. 433 long timestamp = TIMESTAMP_MARKER; 434 for (int messageIndex = 0; messageIndex < numMessages; messageIndex++) { 435 // Sweep numData across critical region of 436 // MidiPortImpl.MAX_PACKET_DATA_SIZE 437 int numData = 1000 + messageIndex; 438 for (int dataIndex = 0; dataIndex < numData; dataIndex++) { 439 buffer[dataIndex] = value; 440 value++; 441 } 442 // This may get split into multiple sends internally. 443 mc.echoInputPort.send(buffer, 0, numData, timestamp); 444 bytesSent += numData; 445 timestamp++; 446 } 447 448 // Check messages. Data must be sequential bytes. 449 value = 0; 450 int bytesReceived = 0; 451 int messageReceivedIndex = 0; 452 int messageSentIndex = 0; 453 int expectedMessageSentIndex = 0; 454 while (bytesReceived < bytesSent) { 455 final int timeoutMs = 500; 456 // Wait for next message. 457 synchronized (receiver) { 458 receiver.waitForMessages(messageReceivedIndex + 1, timeoutMs); 459 } 460 MidiMessage message = receiver.getMessage(messageReceivedIndex++); 461 // parse timestamp marker and data 462 long timestampMarker = message.timestamp & TIMESTAMP_MARKER_MASK; 463 assertEquals("timestamp marker corrupted", TIMESTAMP_MARKER, timestampMarker); 464 messageSentIndex = (int) (message.timestamp & TIMESTAMP_DATA_MASK); 465 466 int numData = message.data.length; 467 for (int dataIndex = 0; dataIndex < numData; dataIndex++) { 468 String msg = String.format("message[%d/%d].data[%d/%d]", 469 messageReceivedIndex, messageSentIndex, dataIndex, 470 numData); 471 assertEquals(msg, value, message.data[dataIndex]); 472 value++; 473 } 474 bytesReceived += numData; 475 // May not advance if message got split 476 if (messageSentIndex > expectedMessageSentIndex) { 477 expectedMessageSentIndex++; // only advance by one each message 478 } 479 assertEquals("timestamp in message", expectedMessageSentIndex, 480 messageSentIndex); 481 } 482 483 mc.echoOutputPort.disconnect(receiver); 484 tearDownEchoServer(mc); 485 } 486 487 // What happens if the app does bad things. 488 public void testEchoBadBehavior() throws Exception { 489 PackageManager pm = mContext.getPackageManager(); 490 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 491 return; // Not supported so don't test it. 492 } 493 MidiTestContext mc = setUpEchoServer(); 494 495 // This should fail because it is already open. 496 MidiInputPort echoInputPort2 = mc.echoDevice.openInputPort(0); 497 assertTrue("input port opened twice", echoInputPort2 == null); 498 499 tearDownEchoServer(mc); 500 } 501 502 // Store history of status changes. 503 private class MyDeviceCallback extends MidiManager.DeviceCallback { 504 private MidiDeviceStatus mStatus; 505 private MidiDeviceInfo mInfo; 506 507 public MyDeviceCallback(MidiDeviceInfo info) { 508 mInfo = info; 509 } 510 511 @Override 512 public synchronized void onDeviceStatusChanged(MidiDeviceStatus status) { 513 super.onDeviceStatusChanged(status); 514 // Filter out status reports from unrelated devices. 515 if (mInfo.equals(status.getDeviceInfo())) { 516 mStatus = status; 517 notifyAll(); 518 } 519 } 520 521 // Wait for a timeout or a notify(). 522 // Return status message or a null if it times out. 523 public synchronized MidiDeviceStatus waitForStatus(int msec) 524 throws InterruptedException { 525 long deadline = System.currentTimeMillis() + msec; 526 long timeRemaining = msec; 527 while (mStatus == null && timeRemaining > 0) { 528 wait(timeRemaining); 529 timeRemaining = deadline - System.currentTimeMillis(); 530 } 531 return mStatus; 532 } 533 534 535 public synchronized void clear() { 536 mStatus = null; 537 } 538 } 539 540 // Test callback for onDeviceStatusChanged(). 541 public void testDeviceCallback() throws Exception { 542 543 PackageManager pm = mContext.getPackageManager(); 544 if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) { 545 return; // Not supported so don't test it. 546 } 547 MidiManager midiManager = (MidiManager) mContext.getSystemService( 548 Context.MIDI_SERVICE); 549 550 MidiDeviceInfo echoInfo = findEchoDevice(); 551 552 // Open device. 553 MyTestOpenCallback callback = new MyTestOpenCallback(); 554 midiManager.openDevice(echoInfo, callback, null); 555 MidiDevice echoDevice = callback.waitForOpen(TIMEOUT_OPEN_MSEC); 556 assertTrue("could not open " + ECHO_PRODUCT, echoDevice != null); 557 MyDeviceCallback deviceCallback = new MyDeviceCallback(echoInfo); 558 try { 559 560 midiManager.registerDeviceCallback(deviceCallback, null); 561 562 MidiDeviceStatus status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC); 563 assertEquals("we should not have any status yet", null, status); 564 565 // Open input port. 566 MidiInputPort echoInputPort = echoDevice.openInputPort(0); 567 assertTrue("could not open input port", echoInputPort != null); 568 569 status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC); 570 assertTrue("should have status by now", null != status); 571 assertEquals("input port open?", true, status.isInputPortOpen(0)); 572 573 deviceCallback.clear(); 574 echoInputPort.close(); 575 status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC); 576 assertTrue("should have status by now", null != status); 577 assertEquals("input port closed?", false, status.isInputPortOpen(0)); 578 579 // Make sure we do NOT get called after unregistering. 580 midiManager.unregisterDeviceCallback(deviceCallback); 581 deviceCallback.clear(); 582 echoInputPort = echoDevice.openInputPort(0); 583 assertTrue("could not open input port", echoInputPort != null); 584 585 status = deviceCallback.waitForStatus(TIMEOUT_STATUS_MSEC); 586 assertEquals("should not get status after unregistering", null, status); 587 588 echoInputPort.close(); 589 } finally { 590 // Safe to call twice. 591 midiManager.unregisterDeviceCallback(deviceCallback); 592 echoDevice.close(); 593 } 594 } 595 } 596