Android Launcher 开发踩坑:USB 设备监听导致文件管理器无法识别 U 盘

问题现象

最近在开发 Android Launcher 时遇到一个奇怪的问题:在 Launcher 中注册了 USB 设备监听后,插入 U 盘时 Launcher 能正常检测到,但这时候打开系统文件管理器,却发现根本看不到 U 盘的挂载信息,就好像 U 盘没插一样。


问题分析

原始实现方式

一开始的实现思路是这样的:

1. 在 Launcher 的 HomeFragment 中注册 USB 设备广播:

intentFilter.addAction("android.hardware.usb.action.USB_DEVICE_ATTACHED");
intentFilter.addAction("android.hardware.usb.action.USB_DEVICE_DETACHED");

2. 收到 USB 插入广播后,使用 libaums 库直接访问 USB 设备:

UsbMassStorageDevice[] devices = UsbMassStorageDevice.getMassStorageDevices(context);
for (UsbMassStorageDevice device : devices) {
    device.init();  // 初始化设备
    FileSystem fs = device.getPartitions().get(0).getFileSystem();
    UsbFile root = fs.getRootDirectory();
    // 查找文件...
}

3. 在 build.gradle 中添加了 libaums 依赖:

implementation 'com.github.mjdev:libaums:0.5.5'

问题根源

这种实现方式有个致命问题:libaums 是基于 USB Host API 直接访问 USB 设备的

当 Launcher 调用 device.init() 后,会通过 USB Host 接口”占用”这个 USB 设备。而且代码里没有调用 device.close() 来释放设备,导致:

  • USB 设备的接口被 Launcher 独占
  • 系统的存储挂载服务无法再访问该设备
  • 文件管理器自然也就看不到 U 盘了

这就像两个人同时想用一个 USB 设备,Launcher 先抢到了,系统就只能干瞪眼。

解决方案

核心思路

既然问题是”抢占设备”导致的,那就别抢了。改用系统的存储挂载机制:

  1. 不再直接访问 USB 设备,而是监听系统的存储挂载广播
  2. 通过系统挂载路径(如 /storage/XXXX-XXXX)来访问文件
  3. 这样 Launcher 和文件管理器都是通过系统挂载点访问,互不干扰

具体实现

1. 修改广播监听(HomeFragment.java)
private void registerReceiver() {
    if (receiver != null) {
        return;
    }
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
    intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);

    // 改为监听存储挂载/卸载事件
    intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
    intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
    intentFilter.addAction(Intent.ACTION_MEDIA_REMOVED);
    intentFilter.addDataScheme("file");  // 重要:必须设置 dataScheme

    receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                // 存储设备挂载(包括 U 盘)
                Uri mountUri = intent.getData();
                if (mountUri != null) {
                    String mountPath = mountUri.getPath();
                    MLog.i(TAG, "Storage mounted at: " + mountPath);

                    // 延迟检测,确保挂载完全完成
                    viewBinding.getRoot().postDelayed(() -> {
                        checkUsbWallpaper(mountPath);
                    }, 1000);
                }
            } else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)
                    || Intent.ACTION_MEDIA_REMOVED.equals(action)) {
                // 存储设备卸载
                Uri unmountUri = intent.getData();
                if (unmountUri != null) {
                    MLog.i(TAG, "Storage unmounted: " + unmountUri.getPath());
                }
            }
        }
    };

    requireContext().registerReceiver(receiver, intentFilter);
}
2. 改造文件检测逻辑(UsbImportUtil.java)
public class UsbImportUtil {

    /**
     * 从系统挂载路径查找壁纸文件
     * 使用系统挂载路径而不是 USB Host,避免抢占设备
     */
    public static File getUsbWallPaperFile(String mountPath) {
        if (mountPath == null || mountPath.isEmpty()) {
            MLog.e(TAG, "Mount path is null or empty");
            return null;
        }

        try {
            File mountDir = new File(mountPath);
            if (!mountDir.exists() || !mountDir.isDirectory()) {
                MLog.e(TAG, "Mount path does not exist or is not a directory: " + mountPath);
                return null;
            }

            // 在挂载目录下查找 wallpaper.png
            File wallpaperFile = new File(mountDir, "wallpaper.png");
            if (wallpaperFile.exists() && wallpaperFile.isFile()) {
                MLog.i(TAG, "Found wallpaper file: " + wallpaperFile.getAbsolutePath());
                return wallpaperFile;
            } else {
                MLog.i(TAG, "Wallpaper file not found in: " + mountPath);
                return null;
            }
        } catch (Exception e) {
            MLog.e(TAG, "Error checking wallpaper file in mount path: " + mountPath, e);
            return null;
        }
    }

    /**
     * 拷贝文件到指定路径
     */
    public static void copyFileToPath(File sourceFile, String destPath) {
        File destFile = new File(destPath);
        if (destFile.exists()) {
            destFile.delete();
        }

        try (FileOutputStream os = new FileOutputStream(destFile);
             FileInputStream is = new FileInputStream(sourceFile)) {

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.flush();
            MLog.i(TAG, "Copy file completed: " + sourceFile.getAbsolutePath() + " -> " + destPath);
        } catch (Exception e) {
            MLog.e(TAG, "Error copying file", e);
        }
    }
}
3. 移除 libaums 依赖(build.gradle)
dependencies {
    // ... 其他依赖

    // 移除这一行
    // implementation 'com.github.mjdev:libaums:0.5.5'
}
4. 清理无用的 import
// 删除这些 import
// import com.github.mjdev.libaums.UsbMassStorageDevice;
// import com.github.mjdev.libaums.fs.FileSystem;
// import com.github.mjdev.libaums.fs.UsbFile;
// import com.github.mjdev.libaums.fs.UsbFileInputStream;

方案对比

对比项USB Host 方式(旧)系统挂载方式(新)
访问方式直接通过 USB Host API通过系统挂载路径
设备占用会独占 USB 接口不占用,共享访问
兼容性可能与系统冲突完全兼容系统行为
依赖库需要 libaums只用标准 API
文件管理器无法访问 ❌正常访问 ✅

测试验证

修改完成后的测试步骤:

  1. 编译安装新版本 Launcher
  2. 在 U 盘根目录放一个 wallpaper.png 文件
  3. 插入 U 盘到设备
  4. 观察 Launcher 是否弹出导入壁纸的对话框
  5. 打开文件管理器,确认能正常看到 U 盘并浏览文件

通过 logcat 可以看到类似日志:

HomeFragment: Storage mounted at: /storage/1234-5678
UsbImportUtil: Found wallpaper file: /storage/1234-5678/wallpaper.png

总结

这个问题的本质是资源竞争

  • USB Host API 是底层访问方式,会独占设备接口
  • 系统存储挂载是上层机制,多个应用可以共享访问

对于 Launcher 这种系统级应用,应该尽量遵循 Android 的标准机制,避免直接操作底层硬件。这样不仅能避免冲突,还能提高兼容性。

如果你也遇到类似的 USB 设备访问问题,不妨检查一下是不是也存在”抢占设备”的情况。

关键要点

  1. 必须设置 addDataScheme("file"):否则收不到存储挂载广播
  2. intent.getData().getPath() 获取挂载路径:这是系统分配的挂载点
  3. 适当延迟检测:挂载过程需要时间,建议延迟 1 秒左右
  4. 使用标准 File API:不需要任何第三方库,兼容性最好

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乡野码圣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值