问题现象
最近在开发 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 先抢到了,系统就只能干瞪眼。
解决方案
核心思路
既然问题是”抢占设备”导致的,那就别抢了。改用系统的存储挂载机制:
- 不再直接访问 USB 设备,而是监听系统的存储挂载广播
- 通过系统挂载路径(如
/storage/XXXX-XXXX)来访问文件 - 这样 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 |
| 文件管理器 | 无法访问 ❌ | 正常访问 ✅ |
测试验证
修改完成后的测试步骤:
- 编译安装新版本 Launcher
- 在 U 盘根目录放一个
wallpaper.png文件 - 插入 U 盘到设备
- 观察 Launcher 是否弹出导入壁纸的对话框
- 打开文件管理器,确认能正常看到 U 盘并浏览文件
通过 logcat 可以看到类似日志:
HomeFragment: Storage mounted at: /storage/1234-5678
UsbImportUtil: Found wallpaper file: /storage/1234-5678/wallpaper.png
总结
这个问题的本质是资源竞争:
- USB Host API 是底层访问方式,会独占设备接口
- 系统存储挂载是上层机制,多个应用可以共享访问
对于 Launcher 这种系统级应用,应该尽量遵循 Android 的标准机制,避免直接操作底层硬件。这样不仅能避免冲突,还能提高兼容性。
如果你也遇到类似的 USB 设备访问问题,不妨检查一下是不是也存在”抢占设备”的情况。
关键要点
- 必须设置
addDataScheme("file"):否则收不到存储挂载广播 - 从
intent.getData().getPath()获取挂载路径:这是系统分配的挂载点 - 适当延迟检测:挂载过程需要时间,建议延迟 1 秒左右
- 使用标准 File API:不需要任何第三方库,兼容性最好
参考资料:

被折叠的 条评论
为什么被折叠?



