Search code examples
javaandroidinterfaceusbmtp

Android MTP client opens a whole device and not a single interface


I have a composite USB gadget that I want to connect to an Android phone. It contains the following serial, MTP, and mass storage interfaces:

interface :: id : 0, name : CDC Abstract Control Model (ACM), alt 0 [0002h:0002h:0001h] CDC Control
interface :: id : 1, name : CDC ACM Data, alt 0 [000ah:0000h:0000h] CDC Data
interface :: id : 2, name : MTP, alt 0 [00ffh:00ffh:0000h] Vendor Specific
interface :: id : 3, name : Mass Storage, alt 0 [0008h:0006h:0050h] Mass Storage

My problem is in attempting to open both the serial and MTP interfaces. Here is my code:

private class SetupInterfacesRunnable implements Runnable
{
    @Override
    public void run()
    {
        synchronized(MyService.this)
        {
            usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
            usbConnection = usbManager.openDevice(usbDevice);

            /*
            interface :: id : 0, name : CDC Abstract Control Model (ACM), alt 0 [0002h:0002h:0001h] CDC Control
            interface :: id : 1, name : CDC ACM Data, alt 0 [000ah:0000h:0000h] CDC Data
            interface :: id : 2, name : MTP, alt 0 [00ffh:00ffh:0000h] Vendor Specific
            interface :: id : 3, name : Mass Storage, alt 0 [0008h:0006h:0050h] Mass Storage
             */

            // Interface 1 on the composite usb device is cdc acm data.
            serialPort = UsbSerialDevice.createUsbSerialDevice(usbDevice, usbConnection, 1);
            if(serialPort != null)
            {
                if(serialPort.open())
                {
                    serialPort.setBaudRate(115200);
                    serialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
                    serialPort.setParity(UsbSerialInterface.PARITY_NONE);
                    serialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
                    serialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);

                    mUIHandler.post(notifyRadgetConnected);

                    // set the callback to catch serial data
                    serialPort.read(mCallback);

                    mUIHandler.post(handshake);

                }else
                {
                    // Serial port could not be opened, maybe an I/O error or it CDC driver was chosen it does not really fit
                    LoggerV2.e("Failed to open device serial port");
                }
            }else
            {
                // No driver for given device, even generic CDC driver could not be loaded
                LoggerV2.e("Failed to find driver for the serial device");
            }

            // Interface 2 on the composite usb device is MTP. 
            MtpDevice mtpDevice = new MtpDevice(usbDevice);
            if (!mtpDevice.open(usbConnection)) {
                LoggerV2.e("Failed to obtain device mtp storage");
            }

        }
    }
}

The serial implementation I am using (felHR85/UsbSerial) is able to open a single interface, however, I cannot see an easy way to implement the MTPDevice class in this manner.

It seems that the Android MTP API wants an entire device/connection to open when the open function is called.

native_open(mDevice.getDeviceName(), connection.getFileDescriptor());

API docs: https://developer.android.com/reference/android/mtp/MtpDevice.html

Source Code: https://android.googlesource.com/platform/frameworks/base/+/master/media/java/android/mtp/MtpDevice.java

I cannot see a way to open only a single interface. Am I missing some trivial way of opening multiple interfaces on the same device using connections?


Solution

  • The short answer/workaround is to run the MTP responder on interface 0 and then open up the serial port after the MTP device.

    The longer answer is... After digging through the code a bit, I found where the native_open call filters through the follow source files:

    • MtpDevice.java
      • android_mtp_MtpDevice.cpp
        • MtpDevice.cpp

    MtpDevice.cpp: https://android.googlesource.com/platform/frameworks/av/+/master/media/mtp/MtpDevice.cpp

    In MtpDevice.cpp, it seems that I would fail with the "endpoints not found\n" error message printed to the log as though it could not find the correct endpoints.

    For now I ended up working around this by reordering the interfaces with MTP first:

    interface :: id : 0, name : MTP, alt 0 [00ffh:00ffh:0000h] Vendor Specific
    interface :: id : 1, name : CDC Abstract Control Model (ACM), alt 0 [0002h:0002h:0001h] CDC Control
    interface :: id : 2, name : CDC ACM Data, alt 0 [000ah:0000h:0000h] CDC Data
    interface :: id : 3, name : Mass Storage, alt 0 [0008h:0006h:0050h] Mass Storage
    

    This allows me to first open the MTP device, then open the serial by interface:

    private class SetupInterfacesRunnable implements Runnable
    {
        @Override
        public void run()
        {
            synchronized(RadgetService.this)
            {
                usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
                usbConnection = usbManager.openDevice(usbDevice);
    
                /*
                interface :: id : 0, name : MTP, alt 0 [00ffh:00ffh:0000h] Vendor Specific
                interface :: id : 1, name : CDC Abstract Control Model (ACM), alt 0 [0002h:0002h:0001h] CDC Control
                interface :: id : 2, name : CDC ACM Data, alt 0 [000ah:0000h:0000h] CDC Data
                interface :: id : 3, name : Mass Storage, alt 0 [0008h:0006h:0050h] Mass Storage
                 */
    
                // Interface 0 on the composite device is MTP
                MtpDevice mtpDevice = new MtpDevice(usbDevice);
                if (!mtpDevice.open(usbConnection)) {
                    LoggerV2.e("Failed to obtain radget mtp storage");
                }
                else
                {
                    LoggerV2.i("Opened MTP storage: %s", mtpDevice.getDeviceName());
    
                    int[] storageIds = mtpDevice.getStorageIds();
                    if (storageIds == null) {
                        LoggerV2.i("No mtp storage id's found");
                        return;
                    }
    
                    /*
                     * scan each storage
                     */
                    for (int storageId : storageIds) {
                        LoggerV2.i("~~~~Storage id: %d~~~~", storageId);
                        scanObjectsInStorage(mtpDevice, storageId, 0, 0);
                    }
                }
    
                // Interface 2 on the composite usb device is cdc acm data.
                serialPort = UsbSerialDevice.createUsbSerialDevice(usbDevice, usbConnection, 2);
                if(serialPort != null)
                {
                    if(serialPort.open())
                    {
                        serialPort.setBaudRate(115200);
                        serialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
                        serialPort.setParity(UsbSerialInterface.PARITY_NONE);
                        serialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
                        serialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
    
                        mUIHandler.post(notifyRadgetConnected);
    
                        // set the callback to catch serial data
                        serialPort.read(mCallback);
    
                        mUIHandler.post(handshake);
    
                    }else
                    {
                        // Serial port could not be opened, maybe an I/O error or it CDC driver was chosen it does not really fit
                        LoggerV2.e("Failed to open device serial port");
                    }
                }else
                {
                    // No driver for given device, even generic CDC driver could not be loaded
                    LoggerV2.e("Failed to find driver for serial device");
                }
    
            }
        }
    }