Android 系统级 USB 存储检测的工程化实现(抗 ROM、抗广播丢失)

在 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,它明确表示:

"设备存在 ≠ 存储可用"


三、整体思路(工程级)

核心思路只有三点:

  1. 启动阶段主动探测(防广播丢失)

  2. 广播只作为触发器

  3. 最终判断以 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;
    }
}
相关推荐
城东米粉儿8 小时前
Android音视频开发基础知识指南
android
莫比乌斯环8 小时前
【Android技能点】一张图理清 开机、App启动流程
android
我命由我123459 小时前
Android Jetpack Compose - Compose 重组、AlertDialog、LazyColumn、Column 与 Row
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
愤怒的代码9 小时前
在 Android 中执行 View.invalidate() 方法后经历了什么
android·java·kotlin
PoppyBu11 小时前
Ubuntu20.04版本上安装最新版本的scrcpy工具
android·ubuntu
执念、坚持11 小时前
Property Service源码分析
android
用户416596736935511 小时前
在 ViewPager2 + Fragment 架构中玩转 Jetpack Compose
android
GoldenPlayer11 小时前
Gradle脚本执行
android
用户745890020795411 小时前
Android进程模型基础
android
we1less11 小时前
[audio] Audio debug
android