Android 设备与 U 盘之间的交互

前言

最近需要实现一个 TV 或一体机从 U 盘读取数据显示的功能,该功能主要解决的问题是:

  • 获取 U 盘根目录
  • 解决拔出 U 盘进程被杀死的问题

一、获取 U 盘根目录

获取 U 盘根目录需要分两种情况:

1.1 应用程序已经在运行,这个时候插入 U 盘。

这种情况我是通过监听媒体挂载的广播来实现的,具体代码如下:
注册广播:

        <receiver
            android:name=".USBBroadcastReceiver">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_MOUNTED"/>
                <action android:name="android.intent.action.MEDIA_UNMOUNTED"/>
                <action android:name="android.intent.action.MEDIA_EJECT"/>

                <data android:scheme="file"/>
            </intent-filter>
        </receiver>

监听 U 盘插入广播并获取 U 盘根目录:

public class USBBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null || intent.getAction() == null) {
            return;
        }
        switch (intent.getAction()) {
            case Intent.ACTION_MEDIA_MOUNTED://扩展介质被插入,而且已经被挂载。
                if (intent.getData() != null) {
                    String path = intent.getData().getPath();
                    String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(path);
                }
                break;
        }
    }
}

经测试,intent.getData().getPath(); 在一体机上获取的并不是 U 盘最终的根目录,所以通过 getUSBRealRootDirectory() 方法再一次提取最终的根目录,该方法具体如下:

    /**
     * 获取 U 盘真正根目录
     *
     * @param usbTempRootDirectory U 盘临时根目录
     * @return U 盘真正根目录
     */
    public static String getUSBRealRootDirectory(String usbTempRootDirectory) {
        String realUSBRootDirectory = "";
        File dir = new File(usbTempRootDirectory);
        File[] files = dir.listFiles();

        /**
         * 注意:
         * 经测试,
         * TV 直接是 usbTempRootDirectory 作为 U 盘的根目录,例如:/storage/577F-85CA
         * 一体机会在 U 盘的根目录(usbTempRootDirectory=/mnt/usb_storage/USB_DISK4)下再创建多个包含 "udisk" 的目录,然后其中一个作为 U 盘的根目录,例如:/mnt/usb_storage/USB_DISK4/udisk0
         */
        if (files != null) {
            for (File file : files) {
                //如果根目录下还有包含 "udisk" 的目录,则该包含 "udisk" 的目录才是 U 盘真正的根目录
                if (file.isDirectory() && file.list().length > 0 && file.getAbsolutePath().contains("udisk")) {
                    realUSBRootDirectory = file.getAbsolutePath();
                    break;
                } else { // 如果根目录下没有包含 "udisk" 的目录,说明 dir 就是根目录
                    realUSBRootDirectory = dir.getAbsolutePath();
                }
            }
        }
        return realUSBRootDirectory;
    }

1.2 应用程序还未运行,U 盘就已经插入了。

这种情况就无法通过监听广播拿到 U 盘根目录了,经查询也没找到特定 API 可以获取到,所以这里只能用反射的方法。具体如下:
通过反射方法获取 U 盘临时根目录

    /**
     * 获取 U 盘临时根目录(一体机会在临时目录下再创建多个包含 "udisk" 的目录,所以临时目录并不是 U 盘真正的根目录)
     *
     * @param context Context
     * @return U 盘临时根目录集合
     */
    public static List<String> getUSBTempRootDirectory(Context context) {
        List<String> usbTempRootDirectory = new ArrayList<>();
        try {
            StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            Class<StorageManager> storageManagerClass = StorageManager.class;
            String[] paths = (String[]) storageManagerClass.getMethod("getVolumePaths").invoke(storageManager);
            for (String path : paths) {
                Object volumeState = storageManagerClass.getMethod("getVolumeState", String.class).invoke(storageManager, path);
                //路劲包含 internal 一般是内部存储,例如 /mnt/internal_sd,需要排除
                if (!path.contains("emulated") && !path.contains("internal") && Environment.MEDIA_MOUNTED.equals(volumeState)) {
                    usbTempRootDirectory.add(path);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return usbTempRootDirectory;
    }

同样,在一体机上获取的并不是 U 盘最终的根目录,所以还是通过 getUSBRealRootDirectory() 方法再一次提取最终的根目录,具体如下:

        List<String> usbTempRootDirectory = FileUtils.getUSBTempRootDirectory(this);
        for (int i = 0; i < usbTempRootDirectory.size(); i++) {
            String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(usbTempRootDirectory.get(i));
        }

二、解决拔出 U 盘进程被杀死的问题

因为需要从 U 盘获取视频地址进行播放,当正在播放的时候拔出 U 盘就会出现进程被杀死的情况,报错日志如下:

ProcessKiller: Process com.xxx.xxx (2088) has open file /mnt/usb_storage/USB_DISK4/udisk0/xxx.mp4
ProcessKiller: Sending SIGHUP to process 2088
Vold: Failed to unmount /mnt/usb_storage/USB_DISK4/udisk0 (Device or resource busy, retries 1, action 2)
ActivityManagerService: Process com.xxx.xxx (pid 2088) has died

这是因为拔出 U 盘的时候,视频资源被视频播放器占用所导致的。可是我明明是做了拔出处理的,即在收到 U 盘被拔出的广播后释放视频资源,如下:

public class USBBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null || intent.getAction() == null) {
            return;
        }
        switch (intent.getAction()) {
            case Intent.ACTION_MEDIA_UNMOUNTED://扩展介质存在,但是还没有被挂载。(扩展介质已被拔出)
                //这里释放所有占用的资源
                break;
        }
    }
}

后来 debug 发现,其实在还未收到 U 盘被拔出的广播,进程就被杀死了。。。

既然不能在监听到 U 盘拔出的时候释放播放资源,那就只能换一种方法了。最后想到的方法是将播放视频的 activity 单独放到一个进程,这样即使该进程被杀死,也不会影响到整个应用奔溃。

虽然通过上面的方法解决了整个应用奔溃的问题,但是还是觉得不完美,总觉得 Android 不可能只提供了 U 盘拔出后的广播,而没有提供 U 盘将要被拔出的广播呀!经过一番查找,嗯,真香!确实有这个广播-android.intent.action.MEDIA_EJECT,该广播表示用户想要移除扩展介质,即扩展介质将要被拔出。收到这个广播释放占用的资源即可,例如视频播放器释放视频资源,文本读写需要关闭流等等。
完整的广播监听如下:

public class USBBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null || intent.getAction() == null) {
            return;
        }
        switch (intent.getAction()) {
            case Intent.ACTION_MEDIA_MOUNTED://扩展介质被插入,而且已经被挂载。
                if (intent.getData() != null) {
                    String path = intent.getData().getPath();
                    String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(path);
                }
                break;
            case Intent.ACTION_MEDIA_EJECT://用户想要移除扩展介质(扩展介质将要被拔出)
                //这里释放所有占用的资源
                break;
            case Intent.ACTION_MEDIA_UNMOUNTED://扩展介质存在,但是还没有被挂载。(扩展介质已被拔出)
                //这里做一些拔出 U 盘后的其他操作
                break;
        }
    }
}

以上就是 Android 设备与 U 盘之间的交互知识,关于获取 U 盘根目录,如果你有更好的方法欢迎交流~

相关源码:AndroidUSB


   转载规则


《Android 设备与 U 盘之间的交互》 wildma 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Android 主流开源框架(一)OkHttp 铺垫-HttpClient 与 HttpURLConnection 使用详解 Android 主流开源框架(一)OkHttp 铺垫-HttpClient 与 HttpURLConnection 使用详解
前言最近有个想法——就是把 Android 主流开源框架进行深入分析,然后写成一系列文章,包括该框架的详细使用与源码解析。目的是通过鉴赏大神的源码来了解框架底层的原理,也就是做到不仅要知其然,还要知其所以然。 这里我说下自己阅读源码的经验,
下一篇 
自己撸一个 Android Studio 插件 自己撸一个 Android Studio 插件
前言用过 Android Studio 进行开发的人一般都使用过插件,因为使用插件可以大大提高我们的开发效率。例如我们常用的插件有: GsonFormat:将 json 数据转换成实体类。 Android Butterknife Zele
  目录