How to programmatically set your app as the default app for a USB device on Android

If you are looking to set your app as the default app for a UsbDevice and do not need to do it programmatically, then I recommend you follow the answers outlined on this SO question. I have a project on GitHub that provides a sample implementation of one of the answers. If you need to set this permission programmatically, then keep reading…

The problem

Initially, we implemented the manual solution outlined in the SO question above on a kiosk type device, but it was still a challenge for our operations team to remember to check the appropriate box before leaving the customer site. This would leave our kiosk device in a state where it was unable to talk to a USB card reader attached to it. We can modify our staging process to do this manual operation, but we also have 100’s of tablets in the field already deployed that need remediation. Our platform runs on a modified tablet that we have root access on. I started to dig through the framework code to see how things work and to see if there was something we could do programmatically.

Some background

When we interact with UsbDevice‘s on Android, we typically do that through the UsbManager service. This is an Android system service that has an implementation exposed to App developers through the Android SDK and a platform implementation that is implemented in the Android Platform source. The SDK implementation (UsbManager) talks to the UsbService using AIDL. When we get the UsbManager service, the methods we call on it call the the IUsbManager interface methods that are implemented by the UsbService on the platform side.

We can see the UsbService getting started by the SystemServer platform code here

                if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)
                        || mPackageManager.hasSystemFeature(
                                PackageManager.FEATURE_USB_ACCESSORY)) {
                    // Manage USB host and device support
                    Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "StartUsbService");
                    mSystemServiceManager.startService(USB_SERVICE_CLASS);
                    Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                }

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/java/com/android/server/SystemServer.java#L988-L994

That block will eventually end up instantiating the UsbService on the platform side here:

        @Override
        public void onStart() {
            mUsbService = new UsbService(getContext());
            publishBinderService(Context.USB_SERVICE, mUsbService);
        }

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbService.java#L65

Don’t lose patience, we are almost to something interesting. The UsbService class handles the management of UsbDevices, but it delegates settings management to a UsbSettingsManager class. The UsbSettingsManager class references a settings file

        mSettingsFile = new AtomicFile(new File(
                Environment.getUserSystemDirectory(user.getIdentifier()),
                "usb_device_manager.xml"));

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbSettingsManager.java#L512

The settings file is read when the UsbSettingsManager is instantiated and the entries inside the file, called DeviceFilter entries, are added to an in memory map:

    // Maps DeviceFilter to user preferred application package
    private final HashMap<DeviceFilter, String> mDevicePreferenceMap =
            new HashMap<DeviceFilter, String>();

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbSettingsManager.java#L89

A sample usb_device_manager.xml file with on DeviceFilter

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<settings>
    <preference package="fs.instore">
        <usb-device vendor-id="2049" product-id="25" class="0" subclass="0" protocol="0" manufacturer-name="MagTek" product-name="USB Swipe Reader" serial-number="B48B67B" />
    </preference>
</settings> 

This is the file that is created when a user connects a UsbDevice, the framework detects that an app has an intent filter to look for this device, and the user selects “Use by default for this USB device” for the particular device. In short, this is the file we need to programmatically create.

Sample dialog:

The usb_device_manager.xml file is located at /data/system/users/0/usb_device_manager.xml on our device.

Lastly, we need to know that this file is only read at USBService start and the in memory map is not exposed publicly. This means that any modifications to this file will require a reboot or service stop/start before they can be used.

The solution

  1. Write the usb_device_manager.xml file with details for our app/device.
  2. Restart the UsbService so that it will read the modified file and use the new values.

Writing usb_device_manager.xml

We can use some of the publicly available platform code to help us write the usb_device_manager.xml file. The framework uses an XML Serializer to write to the settings file

https://github.com/aosp-mirror/platform_frameworks_base/blob/nougat-mr2.3-release/core/java/com/android/internal/util/FastXmlSerializer.java

We can borrow this to write the file or we can use the serializer built into the SDK.

We also need to know the model we need to write. Earlier I mentioned the entries in the XML file were DeviceFilter entries. The DeviceFilter class is a private static inner class of UsbSettingsManager. We can “borrow” this code and the DeviceFilter constructor that takes a UsbDevice and use it in our solution.

                    DeviceFilter deviceFilter = new DeviceFilter(usbDevice);
                    writeSettingsFile(deviceFilter);

Now that we can recreate the same XML as the framework, we need to write to usb_device_manager.xml file. We can modify some of the platform code for our use.


    public static void writeSettingsLocked(Context context, DeviceFilter filter) {

        AtomicFile mSettingsFile = new AtomicFile(new File(context.getExternalFilesDir(null), "usb_device_manager.xml"));

        FileOutputStream fos = null;
        try {
            fos = mSettingsFile.startWrite();

            XmlSerializer serializer = new FastXmlSerializer();
            serializer.setOutput(fos, StandardCharsets.UTF_8.name());
            serializer.startDocument(null, true);
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            serializer.startTag(null, "settings");

                serializer.startTag(null, "preference");
                serializer.attribute(null, "package", "fs.instore");
                filter.write(serializer);
                serializer.endTag(null, "preference");


            serializer.endTag(null, "settings");
            serializer.endDocument();

            mSettingsFile.finishWrite(fos);
        } catch (IOException e) {
            Log.e(TAG, "Failed to write settings", e);
            if (fos != null) {
                mSettingsFile.failWrite(fos);
            }
        }
    }

And once we are done writing the file we need to copy it to the correct location and make sure its file permissions are correct. The following commands are executing programmatically in a shell environment on the kiosk device.

    public static final String COMMAND_COPY_USB_FILE = "cp /sdcard/Android/data/com.whereisdarran.setusbdefault/files/usb_device_manager.xml /data/system/users/0/usb_device_manager.xml";
    public static final String COMMAND_CHOWN_USB_FILE = "chown system:system /data/system/users/0/usb_device_manager.xml";

Lastly, we need to restart the UsbService.

We can do that by simply issuing a reboot command. Alternatively, in a root shell we could issue the stop command followed by a start command to bounce all of the system services.

The full solution.

Thank you for reading!

Leave a comment

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.