本文总结了在 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实现,所以其他版本只能遇到了再处理