在 Android 设备上做 U 盘检测 ,如果只是 Demo,很简单;
但在 系统级 App / 定制 ROM / 量产设备 上跑,事情会迅速变得复杂。
这篇文章分享一个真实工程中可长期运行的 USB 存储检测方案,目标只有一句话:
稳定判断:是否存在可用的 USB 存储,并给出明确状态
一、为什么"监听广播"远远不够
很多示例代码会这么写:
-
监听
USB_DEVICE_ATTACHED -
监听
ACTION_MEDIA_MOUNTED -
收到就认为"U 盘插入了"
但在真实设备上,会遇到这些问题:
-
App 冷启动时,广播已经发完
-
USB 键盘 / 鼠标 / HID 也会发
USB_DEVICE_ATTACHED -
部分 ROM 乱序 / 重复 / 丢失广播
-
U 盘插入了,但 文件系统挂载失败
-
拔盘后还残留旧路径,业务误判
结论很残酷:
广播只能当线索,不能当事实
二、设计目标与状态模型
1️⃣ 设计目标
-
冷启动可检测
-
热插拔可检测
-
不误判 HID
-
挂载异常不死循环
-
状态语义清晰
2️⃣ 状态定义
public enum UsbState {
INIT, // 初始化阶段
NO_USB, // 没有 USB 存储
USB_ATTACHED, // USB 设备存在,但无可用存储
STORAGE_MOUNTED // USB 存储已挂载,可读写
}
这个状态划分非常关键,尤其是 USB_ATTACHED,它明确表示:
"设备存在 ≠ 存储可用"
三、整体思路(工程级)
核心思路只有三点:
-
启动阶段主动探测(防广播丢失)
-
广播只作为触发器
-
最终判断以 StorageManager 为准
四、核心实现代码
1️⃣ 启动时主动检测(兜底)
private void dispatchAppStart() {
handler.postDelayed(this::detectOnStartup, 1000);
}
private void detectOnStartup() {
File root = UsbPathResolver.getUsbRoot(context);
if (root != null) {
usbRoot = root;
setState(UsbState.STORAGE_MOUNTED);
} else if (startupRetry < MAX_RETRY) {
startupRetry++;
handler.postDelayed(this::detectOnStartup, 1000);
} else {
setState(UsbState.NO_USB);
}
}
原因:
-
App 启动时,系统往往已经完成挂载
-
广播无法补发,只能主动探测
2️⃣ USB 插入(不等于存储可用)
private void onUsbAttached() {
if (hasUsb(context)) {
setState(UsbState.STORAGE_MOUNTED);
} else if (currentState == UsbState.NO_USB || currentState == UsbState.INIT) {
setState(UsbState.USB_ATTACHED);
}
}
-
过滤 HID 设备
-
区分"设备存在"和"存储可用"
3️⃣ 存储挂载(带重试上限,防死循环)
private void onStorageMounted() {
File root = UsbPathResolver.getUsbRoot(context);
if (root != null) {
mountRetry = 0;
usbRoot = root;
setState(UsbState.STORAGE_MOUNTED);
} else if (mountRetry++ < MAX_MOUNT_RETRY) {
handler.postDelayed(this::onStorageMounted, 500);
} else {
setState(UsbState.USB_ATTACHED);
}
}
这是整套代码最关键的一段:
-
防止 ROM 发了
MEDIA_MOUNTED但实际不可用 -
防止主线程无限轮询
-
明确区分"挂载失败"和"没有 USB"
4️⃣ 拔出处理(必须清理路径)
private void onUsbDetached() {
usbRoot = null;
setState(UsbState.NO_USB);
}
private void onStorageRemoved() {
usbRoot = null;
setState(UsbState.NO_USB);
}
原则:
状态为 NO_USB,就不应该还能拿到旧路径
5️⃣ 是否存在 USB 存储(最终判定)
public static boolean hasUsb(Context context) {
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (sm == null) return false;
List<StorageVolume> volumes = sm.getStorageVolumes();
for (StorageVolume v : volumes) {
if (v.isRemovable() && v.getState().equals(Environment.MEDIA_MOUNTED)) {
return true;
}
}
return false;
}
这是唯一可信的事实来源。
6️⃣ 查找业务目录(例如 update)
@Nullable
public static File findUpdateDir(File usbRoot) {
if (usbRoot == null || !usbRoot.exists() || !usbRoot.isDirectory()) {
return null;
}
File update = new File(usbRoot, "update");
if (update.exists() && update.isDirectory()) {
return update;
}
return null;
}
五、使用方式示例
UsbManagerHelper usbHelper = new UsbManagerHelper(this);
usbHelper.setListener(state -> {
if (state == UsbManagerHelper.UsbState.STORAGE_MOUNTED) {
File root = usbHelper.getUsbRoot();
File updateDir = UsbManagerHelper.findUpdateDir(root);
if (updateDir != null) {
// 处理逻辑
}
}
});
usbHelper.start();
六、总结(踩坑换来的经验)
这套方案解决了以下真实问题:
-
✅ 冷启动无广播
-
✅ HID 设备误判
-
✅ ROM 广播乱序
-
✅ 挂载异常死循环
-
✅ 拔盘路径残留
核心经验只有一句:
广播不可靠,状态机兜底,StorageManager 才是事实
七、适用场景
-
系统级 App
-
工控 / 医疗 / 车机 / 设备终端
-
U 盘升级、日志导出、数据导入
-
定制 Android ROM
整体代码:
package com.test.test.usb;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.util.Log;
import java.io.File;
import java.util.List;
import androidx.annotation.Nullable;
public class UsbManagerHelper {
private static final String TAG = "=================UsbManagerHelper";
private final Context context;
private UsbState currentState = UsbState.INIT;
private UsbStateListener listener;
private final Handler handler = new Handler(Looper.getMainLooper());
private int startupRetry = 0;
private static final int MAX_RETRY = 5;
private int mountRetry = 0;
private static final int MAX_MOUNT_RETRY = 10;
private File usbRoot;
public UsbManagerHelper(Context context) {
this.context = context.getApplicationContext();
}
public void setListener(UsbStateListener l) {
this.listener = l;
}
public UsbState getCurrentState() {
return currentState;
}
public void start() {
registerReceivers();
dispatchAppStart();
}
public void stop() {
try {
context.unregisterReceiver(receiver);
} catch (Exception ignored) {
}
}
private void dispatchAppStart() {
handler.postDelayed(this::detectOnStartup, 1000);
}
private void detectOnStartup() {
File root = UsbPathResolver.getUsbRoot(context);
if (root != null) {
usbRoot = root;
setState(UsbState.STORAGE_MOUNTED);
} else if (startupRetry < MAX_RETRY) {
startupRetry++;
handler.postDelayed(this::detectOnStartup, 1000);
} else {
setState(UsbState.NO_USB);
}
}
private void onUsbAttached() {
if (hasUsb(context)) {
setState(UsbState.STORAGE_MOUNTED);
} else if (currentState == UsbState.NO_USB || currentState == UsbState.INIT) {
setState(UsbState.USB_ATTACHED);
}
}
private void onUsbDetached() {
usbRoot = null;
setState(UsbState.NO_USB);
}
private void onStorageMounted() {
File root = UsbPathResolver.getUsbRoot(context);
if (root != null) {
mountRetry = 0;
usbRoot = root;
setState(UsbState.STORAGE_MOUNTED);
} else if (mountRetry++ < MAX_MOUNT_RETRY) {
handler.postDelayed(this::onStorageMounted, 500);
} else {
setState(UsbState.USB_ATTACHED);
}
}
private void onStorageRemoved() {
usbRoot = null;
setState(UsbState.NO_USB);
}
public File getUsbRoot() {
return usbRoot;
}
private void setState(UsbState newState) {
if (newState == currentState) return;
currentState = newState;
if (newState != UsbState.INIT) {
startupRetry = MAX_RETRY;
}
if (listener != null) {
listener.onUsbStateChanged(newState);
}
}
// ===================== 广播 =====================
private void registerReceivers() {
IntentFilter usbFilter = new IntentFilter();
usbFilter.addAction("android.hardware.usb.action.USB_DEVICE_ATTACHED");
usbFilter.addAction("android.hardware.usb.action.USB_DEVICE_DETACHED");
context.registerReceiver(receiver, usbFilter);
IntentFilter mediaFilter = new IntentFilter();
mediaFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
mediaFilter.addAction(Intent.ACTION_MEDIA_REMOVED);
mediaFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
mediaFilter.addDataScheme("file");
context.registerReceiver(receiver, mediaFilter);
}
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction();
Log.e(TAG, "action = " + action);
if ("android.hardware.usb.action.USB_DEVICE_ATTACHED".equals(action)) {
onUsbAttached();
} else if ("android.hardware.usb.action.USB_DEVICE_DETACHED".equals(action)) {
onUsbDetached();
} else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
onStorageMounted();
} else if (Intent.ACTION_MEDIA_REMOVED.equals(action)
|| Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) {
onStorageRemoved();
}
}
};
public static boolean hasUsb(Context context) {
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (sm == null) return false;
List<StorageVolume> volumes = sm.getStorageVolumes();
for (StorageVolume v : volumes) {
if (v.isRemovable() && v.getState().equals(Environment.MEDIA_MOUNTED)) {
return true;
}
}
return false;
}
@Nullable
public static File findUpdateDir(File usbRoot) {
if (usbRoot == null || !usbRoot.exists() || !usbRoot.isDirectory()) {
return null;
}
File updata = new File(usbRoot, "update");
if (updata.exists() && updata.isDirectory()) {
return updata;
}
return null;
}
public enum UsbState {
INIT,
NO_USB,
USB_ATTACHED,
STORAGE_MOUNTED
}
public interface UsbStateListener {
void onUsbStateChanged(UsbState newState);
}
}
import android.content.Context;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import java.io.File;
public final class UsbPathResolver {
public static File getUsbRoot(Context context) {
StorageManager sm =
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (sm == null) return null;
for (StorageVolume v : sm.getStorageVolumes()) {
if (!v.isRemovable()) continue;
if (!Environment.MEDIA_MOUNTED.equals(v.getState())) continue;
File dir = v.getDirectory();
if (dir != null && dir.exists()) {
return dir;
}
String uuid = v.getUuid();
if (uuid != null) {
File f = new File("/storage/" + uuid);
if (f.exists()) return f;
}
}
return null;
}
}