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

相关推荐
匆忙拥挤repeat13 分钟前
Android Compose 渲染 UI 帧的三个阶段:组合、布局、绘制
android·ui
帅得不敢出门25 分钟前
Android Studio同一个工程根据不同芯片平台加载不同的framework.jar及使用不同的代码
android·android studio·jar
xiangxiongfly91539 分钟前
Android LeakCanary源码分析
android·leakcanary
黄林晴39 分钟前
紧急预警!Android 17 定位权限大改,你的 App 要适配了
android
夏沫琅琊1 小时前
Android API 发送短信技术文档
android·kotlin
周周不一样1 小时前
Android基础笔记1
android·笔记·gitee
取码网1 小时前
影视APP源码 SK影视 安卓+苹果双端APP 反编译详细视频教程+源码
android
musk12121 小时前
android webview 黑屏问题 , 页面加载时间有点长的情况下
android
夏沫琅琊2 小时前
Android 彩信导出技术文档
android·kotlin