blob: eebcccb68b134a5c2c66aebf3e78b96642645860 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.usb;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.usb.UsbConfiguration;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbRequest;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceServer;
import android.media.midi.MidiDeviceStatus;
import android.media.midi.MidiManager;
import android.media.midi.MidiReceiver;
import android.os.Bundle;
import android.service.usb.UsbDirectMidiDeviceProto;
import android.util.Log;
import com.android.internal.midi.MidiEventScheduler;
import com.android.internal.midi.MidiEventScheduler.MidiEvent;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.server.usb.descriptors.UsbDescriptorParser;
import com.android.server.usb.descriptors.UsbEndpointDescriptor;
import com.android.server.usb.descriptors.UsbInterfaceDescriptor;
import com.android.server.usb.descriptors.UsbMidiBlockParser;
import libcore.io.IoUtils;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* Opens device connections to MIDI 1.0 or MIDI 2.0 endpoints.
* This endpoint will not use ALSA and opens a UsbDeviceConnection directly.
*/
public final class UsbDirectMidiDevice implements Closeable {
private static final String TAG = "UsbDirectMidiDevice";
private static final boolean DEBUG = false;
private Context mContext;
private String mName;
private UsbDevice mUsbDevice;
private UsbDescriptorParser mParser;
private ArrayList<UsbInterfaceDescriptor> mUsbInterfaces;
private final boolean mIsUniversalMidiDevice;
private final String mUniqueUsbDeviceIdentifier;
private final boolean mShouldCallSetInterface;
// USB outputs are MIDI inputs
private final InputReceiverProxy[] mMidiInputPortReceivers;
private final int mNumInputs;
private final int mNumOutputs;
private MidiDeviceServer mServer;
// event schedulers for each input port of the physical device
private MidiEventScheduler[] mEventSchedulers;
// Arbitrary number for timeout to not continue sending to
// an inactive device. This number tries to balances the number
// of cycles and not being permanently stuck.
private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10;
// Arbitrary number for timeout when closing a thread
private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200;
private ArrayList<UsbDeviceConnection> mUsbDeviceConnections;
private ArrayList<ArrayList<UsbEndpoint>> mInputUsbEndpoints;
private ArrayList<ArrayList<UsbEndpoint>> mOutputUsbEndpoints;
private ArrayList<Thread> mThreads;
private UsbMidiBlockParser mMidiBlockParser = new UsbMidiBlockParser();
private int mDefaultMidiProtocol = MidiDeviceInfo.PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS;
private final Object mLock = new Object();
private boolean mIsOpen;
private boolean mServerAvailable;
private UsbMidiPacketConverter mUsbMidiPacketConverter;
private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
@Override
public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
MidiDeviceInfo deviceInfo = status.getDeviceInfo();
int numInputPorts = deviceInfo.getInputPortCount();
int numOutputPorts = deviceInfo.getOutputPortCount();
int numOpenPorts = 0;
for (int i = 0; i < numInputPorts; i++) {
if (status.isInputPortOpen(i)) {
numOpenPorts++;
}
}
for (int i = 0; i < numOutputPorts; i++) {
if (status.getOutputPortOpenCount(i) > 0) {
numOpenPorts += status.getOutputPortOpenCount(i);
}
}
synchronized (mLock) {
Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen
+ " mServerAvailable: " + mServerAvailable);
if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) {
openLocked();
} else if ((numOpenPorts == 0) && mIsOpen) {
closeLocked();
}
}
}
@Override
public void onClose() {
}
};
// This class acts as a proxy for our MidiEventScheduler receivers, which do not exist
// until the device has active clients
private static final class InputReceiverProxy extends MidiReceiver {
private MidiReceiver mReceiver;
@Override
public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
MidiReceiver receiver = mReceiver;
if (receiver != null) {
receiver.send(msg, offset, count, timestamp);
}
}
public void setReceiver(MidiReceiver receiver) {
mReceiver = receiver;
}
@Override
public void onFlush() throws IOException {
MidiReceiver receiver = mReceiver;
if (receiver != null) {
receiver.flush();
}
}
}
/**
* Creates an UsbDirectMidiDevice based on the input parameters. Read/Write streams
* will be created individually as some devices don't have the same number of
* inputs and outputs.
*/
public static UsbDirectMidiDevice create(Context context, UsbDevice usbDevice,
UsbDescriptorParser parser, boolean isUniversalMidiDevice,
String uniqueUsbDeviceIdentifier) {
UsbDirectMidiDevice midiDevice = new UsbDirectMidiDevice(usbDevice, parser,
isUniversalMidiDevice, uniqueUsbDeviceIdentifier);
if (!midiDevice.register(context)) {
IoUtils.closeQuietly(midiDevice);
Log.e(TAG, "createDeviceServer failed");
return null;
}
return midiDevice;
}
private UsbDirectMidiDevice(UsbDevice usbDevice, UsbDescriptorParser parser,
boolean isUniversalMidiDevice, String uniqueUsbDeviceIdentifier) {
mUsbDevice = usbDevice;
mParser = parser;
mUniqueUsbDeviceIdentifier = uniqueUsbDeviceIdentifier;
mIsUniversalMidiDevice = isUniversalMidiDevice;
// Set interface should only be called when alternate interfaces exist.
// Otherwise, USB devices may not handle this gracefully.
mShouldCallSetInterface = (parser.calculateMidiInterfaceDescriptorsCount() > 1);
ArrayList<UsbInterfaceDescriptor> midiInterfaceDescriptors;
if (isUniversalMidiDevice) {
midiInterfaceDescriptors = parser.findUniversalMidiInterfaceDescriptors();
} else {
midiInterfaceDescriptors = parser.findLegacyMidiInterfaceDescriptors();
}
mUsbInterfaces = new ArrayList<UsbInterfaceDescriptor>();
if (mUsbDevice.getConfigurationCount() > 0) {
// USB devices should default to the first configuration.
// The first configuration should support MIDI.
// Only one configuration can be used at once.
// Thus, use USB interfaces from the first configuration.
UsbConfiguration usbConfiguration = mUsbDevice.getConfiguration(0);
for (int interfaceIndex = 0; interfaceIndex < usbConfiguration.getInterfaceCount();
interfaceIndex++) {
UsbInterface usbInterface = usbConfiguration.getInterface(interfaceIndex);
for (UsbInterfaceDescriptor midiInterfaceDescriptor : midiInterfaceDescriptors) {
UsbInterface midiInterface = midiInterfaceDescriptor.toAndroid(mParser);
if (areEquivalent(usbInterface, midiInterface)) {
mUsbInterfaces.add(midiInterfaceDescriptor);
break;
}
}
}
if (mUsbDevice.getConfigurationCount() > 1) {
Log.w(TAG, "Skipping some USB configurations. Count: "
+ mUsbDevice.getConfigurationCount());
}
}
int numInputs = 0;
int numOutputs = 0;
for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) {
UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex);
for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints();
endpointIndex++) {
UsbEndpointDescriptor endpoint =
interfaceDescriptor.getEndpointDescriptor(endpointIndex);
// 0 is output, 1 << 7 is input.
if (endpoint.getDirection() == 0) {
numOutputs++;
} else {
numInputs++;
}
}
}
mNumInputs = numInputs;
mNumOutputs = numOutputs;
Log.d(TAG, "Created UsbDirectMidiDevice with " + numInputs + " inputs and "
+ numOutputs + " outputs. isUniversalMidiDevice: " + isUniversalMidiDevice);
// Create MIDI port receivers based on the number of output ports. The
// output of USB is the input of MIDI.
mMidiInputPortReceivers = new InputReceiverProxy[numOutputs];
for (int port = 0; port < numOutputs; port++) {
mMidiInputPortReceivers[port] = new InputReceiverProxy();
}
}
private int calculateDefaultMidiProtocol() {
UsbManager manager = mContext.getSystemService(UsbManager.class);
for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) {
UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex);
boolean doesInterfaceContainInput = false;
boolean doesInterfaceContainOutput = false;
for (int endpointIndex = 0; (endpointIndex < interfaceDescriptor.getNumEndpoints())
&& !(doesInterfaceContainInput && doesInterfaceContainOutput);
endpointIndex++) {
UsbEndpointDescriptor endpoint =
interfaceDescriptor.getEndpointDescriptor(endpointIndex);
// 0 is output, 1 << 7 is input.
if (endpoint.getDirection() == 0) {
doesInterfaceContainOutput = true;
} else {
doesInterfaceContainInput = true;
}
}
// Intentionally open the device connection to query the default MIDI type for
// a connection with both the input and output set.
if (doesInterfaceContainInput
&& doesInterfaceContainOutput) {
UsbDeviceConnection connection = manager.openDevice(mUsbDevice);
UsbInterface usbInterface = interfaceDescriptor.toAndroid(mParser);
if (!updateUsbInterface(usbInterface, connection)) {
continue;
}
int defaultMidiProtocol = mMidiBlockParser.calculateMidiType(connection,
interfaceDescriptor.getInterfaceNumber(),
interfaceDescriptor.getAlternateSetting());
connection.close();
return defaultMidiProtocol;
}
}
Log.w(TAG, "Cannot find interface with both input and output endpoints");
return MidiDeviceInfo.PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS;
}
private boolean openLocked() {
Log.d(TAG, "openLocked()");
UsbManager manager = mContext.getSystemService(UsbManager.class);
// Converting from raw MIDI to USB MIDI is not thread-safe.
// UsbMidiPacketConverter creates a converter from raw MIDI
// to USB MIDI for each USB output.
mUsbMidiPacketConverter = new UsbMidiPacketConverter(mNumOutputs);
mUsbDeviceConnections = new ArrayList<UsbDeviceConnection>();
mInputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>();
mOutputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>();
mThreads = new ArrayList<Thread>();
for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) {
ArrayList<UsbEndpoint> inputEndpoints = new ArrayList<UsbEndpoint>();
ArrayList<UsbEndpoint> outputEndpoints = new ArrayList<UsbEndpoint>();
UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex);
for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints();
endpointIndex++) {
UsbEndpointDescriptor endpoint =
interfaceDescriptor.getEndpointDescriptor(endpointIndex);
// 0 is output, 1 << 7 is input.
if (endpoint.getDirection() == 0) {
outputEndpoints.add(endpoint.toAndroid(mParser));
} else {
inputEndpoints.add(endpoint.toAndroid(mParser));
}
}
if (!outputEndpoints.isEmpty() || !inputEndpoints.isEmpty()) {
UsbDeviceConnection connection = manager.openDevice(mUsbDevice);
UsbInterface usbInterface = interfaceDescriptor.toAndroid(mParser);
if (!updateUsbInterface(usbInterface, connection)) {
continue;
}
mUsbDeviceConnections.add(connection);
mInputUsbEndpoints.add(inputEndpoints);
mOutputUsbEndpoints.add(outputEndpoints);
}
}
mEventSchedulers = new MidiEventScheduler[mNumOutputs];
for (int i = 0; i < mNumOutputs; i++) {
MidiEventScheduler scheduler = new MidiEventScheduler();
mEventSchedulers[i] = scheduler;
mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver());
}
final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
// Create input thread for each input port of the physical device
int portNumber = 0;
for (int connectionIndex = 0; connectionIndex < mInputUsbEndpoints.size();
connectionIndex++) {
for (int endpointIndex = 0;
endpointIndex < mInputUsbEndpoints.get(connectionIndex).size();
endpointIndex++) {
final UsbDeviceConnection connectionFinal =
mUsbDeviceConnections.get(connectionIndex);
final UsbEndpoint endpointFinal =
mInputUsbEndpoints.get(connectionIndex).get(endpointIndex);
final int portFinal = portNumber;
Thread newThread = new Thread("UsbDirectMidiDevice input thread " + portFinal) {
@Override
public void run() {
final UsbRequest request = new UsbRequest();
try {
request.initialize(connectionFinal, endpointFinal);
byte[] inputBuffer = new byte[endpointFinal.getMaxPacketSize()];
while (true) {
if (Thread.currentThread().interrupted()) {
Log.w(TAG, "input thread interrupted");
break;
}
// Record time of event immediately after waking.
long timestamp = System.nanoTime();
final ByteBuffer byteBuffer = ByteBuffer.wrap(inputBuffer);
if (!request.queue(byteBuffer)) {
Log.w(TAG, "Cannot queue request");
break;
}
final UsbRequest response = connectionFinal.requestWait();
if (response != request) {
Log.w(TAG, "Unexpected response");
break;
}
int bytesRead = byteBuffer.position();
if (bytesRead > 0) {
if (DEBUG) {
logByteArray("Input before conversion ", inputBuffer,
0, bytesRead);
}
byte[] convertedArray;
if (mIsUniversalMidiDevice) {
// For USB, each 32 bit word of a UMP is
// sent with the least significant byte first.
convertedArray = swapEndiannessPerWord(inputBuffer,
bytesRead);
} else {
if (mUsbMidiPacketConverter == null) {
Log.w(TAG, "mUsbMidiPacketConverter is null");
break;
}
convertedArray =
mUsbMidiPacketConverter.usbMidiToRawMidi(
inputBuffer, bytesRead);
}
if (DEBUG) {
logByteArray("Input after conversion ", convertedArray,
0, convertedArray.length);
}
if ((outputReceivers == null)
|| (outputReceivers[portFinal] == null)) {
Log.w(TAG, "outputReceivers is null");
break;
}
outputReceivers[portFinal].send(convertedArray, 0,
convertedArray.length, timestamp);
}
}
} catch (IOException e) {
Log.d(TAG, "reader thread exiting");
} catch (NullPointerException e) {
Log.e(TAG, "input thread: ", e);
} finally {
request.close();
}
Log.d(TAG, "input thread exit");
}
};
newThread.start();
mThreads.add(newThread);
portNumber++;
}
}
// Create output thread for each output port of the physical device
portNumber = 0;
for (int connectionIndex = 0; connectionIndex < mOutputUsbEndpoints.size();
connectionIndex++) {
for (int endpointIndex = 0;
endpointIndex < mOutputUsbEndpoints.get(connectionIndex).size();
endpointIndex++) {
final UsbDeviceConnection connectionFinal =
mUsbDeviceConnections.get(connectionIndex);
final UsbEndpoint endpointFinal =
mOutputUsbEndpoints.get(connectionIndex).get(endpointIndex);
final int portFinal = portNumber;
final MidiEventScheduler eventSchedulerFinal = mEventSchedulers[portFinal];
Thread newThread = new Thread("UsbDirectMidiDevice output thread " + portFinal) {
@Override
public void run() {
try {
while (true) {
if (Thread.currentThread().interrupted()) {
Log.w(TAG, "output thread interrupted");
break;
}
MidiEvent event;
try {
event = (MidiEvent) eventSchedulerFinal.waitNextEvent();
} catch (InterruptedException e) {
Log.w(TAG, "event scheduler interrupted");
break;
}
if (event == null) {
Log.w(TAG, "event is null");
break;
}
if (DEBUG) {
logByteArray("Output before conversion ", event.data, 0,
event.count);
}
byte[] convertedArray;
if (mIsUniversalMidiDevice) {
// For USB, each 32 bit word of a UMP is
// sent with the least significant byte first.
convertedArray = swapEndiannessPerWord(event.data,
event.count);
} else {
if (mUsbMidiPacketConverter == null) {
Log.w(TAG, "mUsbMidiPacketConverter is null");
break;
}
convertedArray =
mUsbMidiPacketConverter.rawMidiToUsbMidi(
event.data, event.count, portFinal);
}
if (DEBUG) {
logByteArray("Output after conversion ", convertedArray, 0,
convertedArray.length);
}
connectionFinal.bulkTransfer(endpointFinal, convertedArray,
convertedArray.length,
BULK_TRANSFER_TIMEOUT_MILLISECONDS);
eventSchedulerFinal.addEventToPool(event);
}
} catch (NullPointerException e) {
Log.e(TAG, "output thread: ", e);
}
Log.d(TAG, "output thread exit");
}
};
newThread.start();
mThreads.add(newThread);
portNumber++;
}
}
mIsOpen = true;
return true;
}
private boolean register(Context context) {
mContext = context;
MidiManager midiManager = context.getSystemService(MidiManager.class);
if (midiManager == null) {
Log.e(TAG, "No MidiManager in UsbDirectMidiDevice.register()");
return false;
}
if (mIsUniversalMidiDevice) {
mDefaultMidiProtocol = calculateDefaultMidiProtocol();
} else {
mDefaultMidiProtocol = MidiDeviceInfo.PROTOCOL_UNKNOWN;
}
Bundle properties = new Bundle();
String manufacturer = mUsbDevice.getManufacturerName();
String product = mUsbDevice.getProductName();
String version = mUsbDevice.getVersion();
String name;
if (manufacturer == null || manufacturer.isEmpty()) {
name = product;
} else if (product == null || product.isEmpty()) {
name = manufacturer;
} else {
name = manufacturer + " " + product;
}
name += "#" + mUniqueUsbDeviceIdentifier;
if (mIsUniversalMidiDevice) {
name += " MIDI 2.0";
} else {
name += " MIDI 1.0";
}
mName = name;
properties.putString(MidiDeviceInfo.PROPERTY_NAME, name);
properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer);
properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product);
properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version);
properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
mUsbDevice.getSerialNumber());
properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, mUsbDevice);
mServerAvailable = true;
mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs,
null, null, properties, MidiDeviceInfo.TYPE_USB, mDefaultMidiProtocol, mCallback);
if (mServer == null) {
return false;
}
return true;
}
@Override
public void close() throws IOException {
synchronized (mLock) {
if (mIsOpen) {
closeLocked();
}
mServerAvailable = false;
}
if (mServer != null) {
IoUtils.closeQuietly(mServer);
}
}
private void closeLocked() {
Log.d(TAG, "closeLocked()");
// Send an interrupt signal to threads.
for (Thread thread : mThreads) {
if (thread != null) {
thread.interrupt();
}
}
// Wait for threads to actually stop.
for (Thread thread : mThreads) {
if (thread != null) {
try {
thread.join(THREAD_JOIN_TIMEOUT_MILLISECONDS);
} catch (InterruptedException e) {
Log.w(TAG, "thread join interrupted");
break;
}
}
}
mThreads = null;
for (int i = 0; i < mEventSchedulers.length; i++) {
mMidiInputPortReceivers[i].setReceiver(null);
mEventSchedulers[i].close();
}
mEventSchedulers = null;
for (UsbDeviceConnection connection : mUsbDeviceConnections) {
connection.close();
}
mUsbDeviceConnections = null;
mInputUsbEndpoints = null;
mOutputUsbEndpoints = null;
mUsbMidiPacketConverter = null;
mIsOpen = false;
}
private byte[] swapEndiannessPerWord(byte[] inputArray, int size) {
int numberOfExcessBytes = size & 3;
if (numberOfExcessBytes != 0) {
Log.e(TAG, "size not multiple of 4: " + size);
}
byte[] outputArray = new byte[size - numberOfExcessBytes];
for (int i = 0; i + 3 < size; i += 4) {
outputArray[i] = inputArray[i + 3];
outputArray[i + 1] = inputArray[i + 2];
outputArray[i + 2] = inputArray[i + 1];
outputArray[i + 3] = inputArray[i];
}
return outputArray;
}
private static void logByteArray(String prefix, byte[] value, int offset, int count) {
StringBuilder builder = new StringBuilder(prefix);
for (int i = offset; i < offset + count; i++) {
builder.append(String.format("0x%02X", value[i]));
if (i != value.length - 1) {
builder.append(", ");
}
}
Log.d(TAG, builder.toString());
}
private boolean updateUsbInterface(UsbInterface usbInterface,
UsbDeviceConnection connection) {
if (usbInterface == null) {
Log.e(TAG, "Usb Interface is null");
return false;
}
if (connection == null) {
Log.e(TAG, "UsbDeviceConnection is null");
return false;
}
if (!connection.claimInterface(usbInterface, true)) {
Log.e(TAG, "Can't claim interface");
return false;
}
if (mShouldCallSetInterface) {
if (!connection.setInterface(usbInterface)) {
Log.w(TAG, "Can't set interface");
}
} else {
Log.w(TAG, "no alternate interface");
}
return true;
}
private boolean areEquivalent(UsbInterface interface1, UsbInterface interface2) {
if ((interface1.getId() != interface2.getId())
|| (interface1.getAlternateSetting() != interface2.getAlternateSetting())
|| (interface1.getInterfaceClass() != interface2.getInterfaceClass())
|| (interface1.getInterfaceSubclass() != interface2.getInterfaceSubclass())
|| (interface1.getInterfaceProtocol() != interface2.getInterfaceProtocol())
|| (interface1.getEndpointCount() != interface2.getEndpointCount())) {
return false;
}
if (interface1.getName() == null) {
if (interface2.getName() != null) {
return false;
}
} else if (!(interface1.getName().equals(interface2.getName()))) {
return false;
}
// Consider devices with the same endpoints but in a different order as different endpoints.
for (int i = 0; i < interface1.getEndpointCount(); i++) {
UsbEndpoint endpoint1 = interface1.getEndpoint(i);
UsbEndpoint endpoint2 = interface2.getEndpoint(i);
if ((endpoint1.getAddress() != endpoint2.getAddress())
|| (endpoint1.getAttributes() != endpoint2.getAttributes())
|| (endpoint1.getMaxPacketSize() != endpoint2.getMaxPacketSize())
|| (endpoint1.getInterval() != endpoint2.getInterval())) {
return false;
}
}
return true;
}
/**
* Write a description of the device to a dump stream.
*/
public void dump(@NonNull DualDumpOutputStream dump, @NonNull String idName, long id) {
long token = dump.start(idName, id);
dump.write("num_inputs", UsbDirectMidiDeviceProto.NUM_INPUTS, mNumInputs);
dump.write("num_outputs", UsbDirectMidiDeviceProto.NUM_OUTPUTS, mNumOutputs);
dump.write("is_universal", UsbDirectMidiDeviceProto.IS_UNIVERSAL, mIsUniversalMidiDevice);
dump.write("name", UsbDirectMidiDeviceProto.NAME, mName);
if (mIsUniversalMidiDevice) {
mMidiBlockParser.dump(dump, "block_parser", UsbDirectMidiDeviceProto.BLOCK_PARSER);
}
dump.end(token);
}
}