Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android外接U盘

Android外接U盘的操作实践

作者:没有了遇见

本文总结了在 Android(开发板 Android 14)环境下外接 U 盘的操作实践,涵盖了从配置、权限申请、USB 设备识别,到文件系统读取和本地文件复制的完整流程,本文内容适合需要在 Android 上进行 U 盘数据处理的开发者参考和实践,需要的朋友可以参考下

引言

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

操作流程

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

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

1.1 权限配置

<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类型文件配置

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

device_filter.xml

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

注意:

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

2:读取U盘数据

读取U盘的具体步骤

2.1 获取U盘信息

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

2.1.1 权限申请的方法

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 广播监听

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 获取数据

判断权限

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盘数据

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 开源库

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 {
// 广播接收器
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 文件复制工具类

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 调用示例

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");
    }

}

注意:

总结

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

以上就是Android外接U盘的操作实践的详细内容,更多关于Android外接U盘的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文