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