Home | History | Annotate | Download | only in cts
      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