Android 外接 U 盘开发实战:从权限到文件复制

本文总结了在 Android(开发板 Android 14)环境下外接 U 盘的操作实践,涵盖了从配置、权限申请、USB 设备识别,到文件系统读取和本地文件复制的完整流程。文章重点介绍了高版本 Android 的存储权限处理、BroadcastReceiver 异步回调机制,以及利用开源库 libaums 安全高效地读取 U 盘文件的实现方式。同时提供了递归复制 U 盘目录到本地的工具类及进度回调接口,实现了对视频、音乐等大文件的稳定复制。本文内容适合需要在 Android 上进行 U 盘数据处理的开发者参考和实践。

操作流程

  • 配置
  • 权限
  • 获取外接设备列表
  • 获取根目录
  • 复制文件到本地

1:使用USB外接设备的配置

使用USB的时候的配置分为,权限 过滤文件

1.1 权限配置

ini 复制代码
<uses-feature android:name="android.hardware.usb.host" />

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

⚠️ Android 10+ 需要申请 存储访问权限 ,Android 11+ 可以使用 MANAGE_EXTERNAL_STORAGE 或 SAF(Storage Access Framework)。

1.2 过滤USB类型文件配置

ini 复制代码
<application
...
<meta-data
    android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
    android:resource="@xml/device_filter" />
    
 </application>

device_filter.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="0x1234" product-id="0x5678" />
</resources>

注意:

  • 不配置会处理所有 USB Mass Storage 设备

  • 配置可以避免鼠标、键盘等干扰

我这里没处理这个配置,是获取判断外接设备后 只处理U盘的处理方式

2:读取U盘数据

读取U盘的具体步骤

  • 申请U盘权限
  • 获取U盘数据
  • Copy数据

2.1 获取U盘信息

在 Android 中申请 U 盘权限通常是通过 UsbManager + PendingIntent + BroadcastReceiver 来完成的

2.1.1 权限申请的方法
kotlin 复制代码
fun requestPermission(context: Context, usbManager: UsbManager, device: UsbDevice?) {
 val intent = Intent(MediaShowConstant.ACTION_USB_PERMISSION).apply {
            putExtra(UsbManager.EXTRA_DEVICE, device)
        }

        // Android 12+ 要求 PendingIntent 必须是 MUTABLE
        val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }

        val permissionIntent = PendingIntent.getBroadcast(context, 0, intent, flags)

        Log.d("UsbHelper", "申请 U 盘权限: ${device.deviceName}")
        usbManager.requestPermission(device, permissionIntent)
}
2.1.2 广播监听
kotlin 复制代码
class CustomUsbPermissionReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action ?: return
        val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return

        Log.d("UsbHelper", "收到广播: $action, device=${device.deviceName}")

        when (action) {
            UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                if (!isMassStorageDevice(device)) return
                requestPermission(context, usbManager, device)
            }
            MediaShowConstant.ACTION_USB_PERMISSION -> {
                val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
                pendingDeviceIds.remove(device.deviceId)
                if (granted && isMassStorageDevice(device)) {
                    Log.d("UsbHelper", "USB 权限已授权,开始加载文件系统")
                    loadUsbFile(context, device)
                } else {
                    Log.w("UsbHelper", "USB 权限被拒绝: ${device.deviceName}")
                }
            }
            UsbManager.ACTION_USB_DEVICE_DETACHED -> {
                if (isMassStorageDevice(device) && initializedDeviceIds.remove(device.deviceId)) {
                    Log.d("UsbHelper", "U盘拔出,移除初始化标记: ${device.deviceName}")
                    usbRemovedCallback?.invoke(device)
                }
            }
        }
    }
}

2.2 获取数据

判断权限

kotlin 复制代码
private fun checkUsbDevices(context: Context) {
    val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
    for (device in usbManager.deviceList.values) {
        if (!isMassStorageDevice(device)) continue
        if (!usbManager.hasPermission(device)) {
            requestPermission(context, usbManager, device)
        } else {
            loadUsbFile(context, device)
        }
    }
}

获取U盘数据

kotlin 复制代码
private fun loadUsbFile(context: Context, device: UsbDevice) {
    Log.d("UsbHelper", "尝试加载 USB: ${device.deviceName}")

    val devices = UsbMassStorageDevice.getMassStorageDevices(context)
    for (usbDevice in devices) {
        if (usbDevice.usbDevice.deviceId != device.deviceId) continue
        try {
            Log.d("UsbHelper", "初始化 USB: ${device.deviceName}")
            usbDevice.init()

            // 只加载第一个分区(如需多分区可遍历 partitions)
            val fs = usbDevice.partitions[0].fileSystem
            initializedDeviceIds.add(device.deviceId)

            Log.d("UsbHelper", "调用回调,U盘已初始化")
            loadFileCallback?.invoke(fs, fs.rootDirectory)
        } catch (e: Exception) {
            Log.e("UsbHelper", "USB 初始化异常: ${e.message}", e)
        }
    }
}

注意:

获取u盘数据用的libaums 开源库

arduino 复制代码
implementation  'me.jahnen.libaums:core:0.10.0'

3.文件Copy

将U盘识别和文件读取分装成一个两个工具类.

3.1 盘识别工具类

import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.hardware.usb.UsbConstants import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log import com.wkq.common.util.showToast import me.jahnen.libaums.core.UsbMassStorageDevice import me.jahnen.libaums.core.fs.FileSystem import me.jahnen.libaums.core.fs.UsbFile

object UsbPermissionHelper {

kotlin 复制代码
// 广播接收器
private var usbPermissionReceiver: CustomUsbPermissionReceiver? = null
// 待申请权限的设备集合,避免重复申请
private val pendingDeviceIds = mutableSetOf<Int>()
// 已初始化的设备集合,方便拔出处理
private val initializedDeviceIds = mutableSetOf<Int>()
// U盘文件加载回调
var loadFileCallback: ((fs: FileSystem, rootDir: UsbFile) -> Unit)? = null
// U盘拔出回调
var usbRemovedCallback: ((device: UsbDevice) -> Unit)? = null
// 广播是否已注册
private var isReceiverRegistered = false

/**
 * 入口方法:处理 USB 权限和文件系统加载
 */
fun processUsb(context: Context, loadFileCallback: (fs: FileSystem, rootDir: UsbFile) -> Unit) {
    if (isReceiverRegistered) return

    this.loadFileCallback = loadFileCallback
    val appContext = context.applicationContext

    // 检查设备是否支持 USB HOST
    if (!appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
        appContext.showToast("设备不支持 USB HOST")
        return
    }

    // 注册广播接收器
    usbPermissionReceiver = CustomUsbPermissionReceiver()
    val filter = IntentFilter().apply {
        addAction(MediaShowConstant.ACTION_USB_PERMISSION) // 自定义权限广播
        addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)   // U盘插入广播
        addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)   // U盘拔出广播
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        appContext.registerReceiver(usbPermissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
    } else {
        @Suppress("DEPRECATION")
        appContext.registerReceiver(usbPermissionReceiver, filter)
    }
    isReceiverRegistered = true

    // 检查当前已连接的 USB 设备
    checkUsbDevices(appContext)
}

/**
 * 检查当前已连接的 USB 设备并处理
 */
private fun checkUsbDevices(context: Context) {
    val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
    for (device in usbManager.deviceList.values) {
        if (!isMassStorageDevice(device)) continue  // 只处理存储类设备
        if (!usbManager.hasPermission(device)) {
            requestPermission(context, usbManager, device) // 请求权限
        } else {
            loadUsbFile(context, device) // 已有权限直接加载
        }
    }
}

/**
 * 判断设备是否为 USB 存储类
 */
private fun isMassStorageDevice(device: UsbDevice): Boolean {
    for (i in 0 until device.interfaceCount) {
        if (device.getInterface(i).interfaceClass == UsbConstants.USB_CLASS_MASS_STORAGE) return true
    }
    return false
}

/**
 * 加载 USB 文件系统
 */
private fun loadUsbFile(context: Context, device: UsbDevice) {
    Log.d("UsbHelper", "尝试加载 USB: ${device.deviceName}")

    val devices = UsbMassStorageDevice.getMassStorageDevices(context)
    for (usbDevice in devices) {
        if (usbDevice.usbDevice.deviceId != device.deviceId) continue
        try {
            Log.d("UsbHelper", "初始化 USB: ${device.deviceName}")
            usbDevice.init() // 初始化 USB 设备

            // 只加载第一个分区(如需多分区可遍历 partitions)
            val fs = usbDevice.partitions[0].fileSystem
            initializedDeviceIds.add(device.deviceId) // 标记已初始化

            Log.d("UsbHelper", "调用回调,U盘已初始化")
            loadFileCallback?.invoke(fs, fs.rootDirectory) // 回调文件系统
        } catch (e: Exception) {
            Log.e("UsbHelper", "USB 初始化异常: ${e.message}", e)
        }
    }
}

/**
 * 释放资源
 */
fun release(context: Context) {
    val appContext = context.applicationContext
    if (isReceiverRegistered && usbPermissionReceiver != null) {
        try { appContext.unregisterReceiver(usbPermissionReceiver) } catch (_: IllegalArgumentException) {}
        usbPermissionReceiver = null
        isReceiverRegistered = false
    }
    loadFileCallback = null
    usbRemovedCallback = null
    pendingDeviceIds.clear()
    initializedDeviceIds.clear()
}

/**
 * 请求 USB 权限
 */
fun requestPermission(context: Context, usbManager: UsbManager, device: UsbDevice?) {
    device ?: return
    if (pendingDeviceIds.contains(device.deviceId)) return

    pendingDeviceIds.add(device.deviceId)
    Handler(Looper.getMainLooper()).postDelayed({
        val intent = Intent(MediaShowConstant.ACTION_USB_PERMISSION).apply {
            putExtra(UsbManager.EXTRA_DEVICE, device)
        }

        // Android 12+ 要求 PendingIntent 必须是 MUTABLE
        val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }

        val permissionIntent = PendingIntent.getBroadcast(context, 0, intent, flags)

        Log.d("UsbHelper", "申请 U 盘权限: ${device.deviceName}")
        usbManager.requestPermission(device, permissionIntent)
    }, 200) // 延迟 200ms 避免广播未注册
}

/**
 * 自定义广播接收器
 */
class CustomUsbPermissionReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action ?: return
        val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return

        Log.d("UsbHelper", "收到广播: $action, device=${device.deviceName}")

        when (action) {
            UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                if (!isMassStorageDevice(device)) return
                requestPermission(context, usbManager, device) // 插入时请求权限
            }
            MediaShowConstant.ACTION_USB_PERMISSION -> {
                val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
                pendingDeviceIds.remove(device.deviceId)
                if (granted && isMassStorageDevice(device)) {
                    Log.d("UsbHelper", "USB 权限已授权,开始加载文件系统")
                    loadUsbFile(context, device) // 权限允许后加载文件
                } else {
                    Log.w("UsbHelper", "USB 权限被拒绝: ${device.deviceName}")
                }
            }
            UsbManager.ACTION_USB_DEVICE_DETACHED -> {
                if (isMassStorageDevice(device) && initializedDeviceIds.remove(device.deviceId)) {
                    Log.d("UsbHelper", "U盘拔出,移除初始化标记: ${device.deviceName}")
                    usbRemovedCallback?.invoke(device) // 回调拔出事件
                }
            }
        }
    }
}

}

3.2 文件复制工具类

scss 复制代码
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.jahnen.libaums.core.fs.FileSystem
import me.jahnen.libaums.core.fs.UsbFile
import me.jahnen.libaums.core.fs.UsbFileStreamFactory
import java.io.File
import java.io.FileOutputStream

object UsbFileCopyUtil {

    private const val TAG = "UsbFileCopyUtil"

    /**
     * 复制整个 USB 目录(递归方式),保证视频和音乐文件都能正常复制
     */
    suspend fun copyUsbDirToCache(
        usbDir: UsbFile,
        fs: FileSystem,
        targetDir: File,
        filter: ((UsbFile) -> Boolean)? = null,
        callback: CopyProgressCallback? = null
    ): Boolean = withContext(Dispatchers.IO) {
        try {
            if (!usbDir.isDirectory) return@withContext false

            // 确保目标根目录存在
            if (!targetDir.exists() && !targetDir.mkdirs()) {
                Log.e(TAG, "Failed to create targetDir: ${targetDir.absolutePath}")
                return@withContext false
            }

            // 收集所有待复制文件
            val fileList = mutableListOf<Pair<UsbFile, File>>()
            collectUsbFiles(usbDir, targetDir, filter, fileList)

            // 回调总数(在主线程)
            Handler(Looper.getMainLooper()).post {
                callback?.onStart(fileList.size)
            }

            var current = 0
            var allSuccess = true

            for ((source, target) in fileList) {
                try {
                    // 创建父目录
                    val parent = target.parentFile
                    if (parent != null && !parent.exists() && !parent.mkdirs()) {
                        Log.e(TAG, "Failed to create parent directory: ${parent.absolutePath}")
                        allSuccess = false
                        continue
                    }

                    // 跳过同名且大小相同的文件
                    if (target.exists() && target.length() == source.length) {
                        Log.d(TAG, "Skip same-size file: ${target.absolutePath}")
                        current++
                        Handler(Looper.getMainLooper()).post {
                            callback?.onFileCopied(current, fileList.size, source, target)
                        }
                        continue
                    }

                    // 安全文件名
                    val safeName = source.name.replace("[\\/:*?"<>|]".toRegex(), "_")
                    val safeTarget = File(target.parentFile, safeName)

                    // 手动循环读取,确保所有文件(包括 MP3)都能完整写入
                    UsbFileStreamFactory.createBufferedInputStream(source, fs).use { input ->
                        FileOutputStream(safeTarget).use { output ->
                            val buffer = ByteArray(64 * 1024)
                            var read: Int
                            while (true) {
                                read = input.read(buffer)
                                if (read == -1) break
                                output.write(buffer, 0, read)
                            }
                            output.flush()
                        }
                    }

                    current++
                    Handler(Looper.getMainLooper()).post {
                        callback?.onFileCopied(current, fileList.size, source, safeTarget)
                    }

                } catch (e: Exception) {
                    allSuccess = false
                    Log.e(TAG, "Failed to copy file: ${source.name}, target=${target.absolutePath}", e)
                    Handler(Looper.getMainLooper()).post {
                        callback?.onError(source, e)
                    }
                }
            }

            // 完成回调
            Handler(Looper.getMainLooper()).post {
                callback?.onComplete(allSuccess)
            }

            return@withContext allSuccess

        } catch (e: Exception) {
            Log.e(TAG, "Failed to copy USB directory", e)
            Handler(Looper.getMainLooper()).post {
                callback?.onError(usbDir, e)
                callback?.onComplete(false)
            }
            return@withContext false
        }
    }

    /**
     * 递归收集 USB 文件夹内所有文件,保持目录层级一致
     */
    private fun collectUsbFiles(
        usbDir: UsbFile,
        targetDir: File,
        filter: ((UsbFile) -> Boolean)?,
        outList: MutableList<Pair<UsbFile, File>>
    ) {
        for (child in usbDir.listFiles()) {
            if (filter != null && !filter(child)) continue

            val target = File(targetDir, child.name)
            if (child.isDirectory) {
                collectUsbFiles(child, target, filter, outList)
            } else {
                outList.add(child to target)
            }
        }
    }

    interface CopyProgressCallback {
        fun onStart(totalCount: Int)
        fun onFileCopied(current: Int, total: Int, source: UsbFile, target: File)
        fun onError(source: UsbFile, e: Exception)
        fun onComplete(success: Boolean)
    }
}

3.3 调用示例

kotlin 复制代码
UsbPermissionHelper.processUsb(this) { fs, rootDir ->
    val files = rootDir.listFiles()
    files.iterator().forEach {
        Log.d("UsbHelper:", "File: " + it.name)
    }
    val usbDir = rootDir.search("MediaFolder")
    if (usbDir != null && usbDir.isDirectory && usbDir.listFiles().size > 0) {
     copyFile(fs, usbDir)
    } else {
        this.showToast("The USB is not recognized to contain available files");
    }

}

注意:

  • 高版本的Android 的存读取权限的申请.
  • 读文件三方库
    • implementation 'me.jahnen.libaums:core:0.10.0'

总结

U盘数据读取分为 配置,权限申请 获取数据 复制文件几步.这里总结了一下,因为是开发板Android 14实现,所以其他版本只能遇到了再处理

相关推荐
Android轮子哥14 分钟前
尝试解决 Android 适配最后一公里
android
雨白1 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android
风往哪边走2 小时前
自定义仿日历组件弹框
android
Monkey-旭3 小时前
Android 文件存储机制全解析
android·文件存储·kolin
zhangphil4 小时前
Android Coil 3拦截器Interceptor计算单次请求耗时,Kotlin
android·kotlin
DokiDoki之父4 小时前
多线程—飞机大战排行榜功能(2.0版本)
android·java·开发语言
用户2018792831676 小时前
强制关闭生命周期延时的Activity实现思路
android
用户2018792831676 小时前
Activity后生命周期暂停问题
android
用户2018792831676 小时前
浅析:WindowManager添加的 View 的事件传递机制
android