Android 设备与 U 盘之间的交互

作者: wildma | 来源:发表于2019-07-28 23:35 被阅读20次

    前言

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

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

    获取 U 盘根目录

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

    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;
        }
    

    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 盘之间的交互

        本文链接:https://www.haomeiwen.com/subject/tvflrctx.html