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