Android外接U盤(pán)的操作實(shí)踐
引言
本文總結(jié)了在 Android(開(kāi)發(fā)板 Android 14)環(huán)境下外接 U 盤(pán)的操作實(shí)踐,涵蓋了從配置、權(quán)限申請(qǐng)、USB 設(shè)備識(shí)別,到文件系統(tǒng)讀取和本地文件復(fù)制的完整流程。文章重點(diǎn)介紹了高版本 Android 的存儲(chǔ)權(quán)限處理、BroadcastReceiver 異步回調(diào)機(jī)制,以及利用開(kāi)源庫(kù) libaums 安全高效地讀取 U 盤(pán)文件的實(shí)現(xiàn)方式。同時(shí)提供了遞歸復(fù)制 U 盤(pán)目錄到本地的工具類及進(jìn)度回調(diào)接口,實(shí)現(xiàn)了對(duì)視頻、音樂(lè)等大文件的穩(wěn)定復(fù)制。本文內(nèi)容適合需要在 Android 上進(jìn)行 U 盤(pán)數(shù)據(jù)處理的開(kāi)發(fā)者參考和實(shí)踐。
操作流程
- 配置
- 權(quán)限
- 獲取外接設(shè)備列表
- 獲取根目錄
- 復(fù)制文件到本地
1:使用USB外接設(shè)備的配置
使用USB的時(shí)候的配置分為,權(quán)限 過(guò)濾文件
1.1 權(quán)限配置
<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+ 需要申請(qǐng) 存儲(chǔ)訪問(wèn)權(quán)限,Android 11+ 可以使用 MANAGE_EXTERNAL_STORAGE 或 SAF(Storage Access Framework)。
1.2 過(guò)濾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>
注意:
- 不配置會(huì)處理所有 USB Mass Storage 設(shè)備
- 配置可以避免鼠標(biāo)、鍵盤(pán)等干擾
我這里沒(méi)處理這個(gè)配置,是獲取判斷外接設(shè)備后 只處理U盤(pán)的處理方式
2:讀取U盤(pán)數(shù)據(jù)
讀取U盤(pán)的具體步驟
- 申請(qǐng)U盤(pán)權(quán)限
- 獲取U盤(pán)數(shù)據(jù)
- Copy數(shù)據(jù)
2.1 獲取U盤(pán)信息
在 Android 中申請(qǐng) U 盤(pán)權(quán)限通常是通過(guò) UsbManager + PendingIntent + BroadcastReceiver 來(lái)完成的
2.1.1 權(quán)限申請(qǐng)的方法
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", "申請(qǐng) U 盤(pán)權(quán)限: ${device.deviceName}")
usbManager.requestPermission(device, permissionIntent)
}
2.1.2 廣播監(jiān)聽(tīng)
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 權(quán)限已授權(quán),開(kāi)始加載文件系統(tǒng)")
loadUsbFile(context, device)
} else {
Log.w("UsbHelper", "USB 權(quán)限被拒絕: ${device.deviceName}")
}
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
if (isMassStorageDevice(device) && initializedDeviceIds.remove(device.deviceId)) {
Log.d("UsbHelper", "U盤(pán)拔出,移除初始化標(biāo)記: ${device.deviceName}")
usbRemovedCallback?.invoke(device)
}
}
}
}
}
2.2 獲取數(shù)據(jù)
判斷權(quán)限
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盤(pán)數(shù)據(jù)
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()
// 只加載第一個(gè)分區(qū)(如需多分區(qū)可遍歷 partitions)
val fs = usbDevice.partitions[0].fileSystem
initializedDeviceIds.add(device.deviceId)
Log.d("UsbHelper", "調(diào)用回調(diào),U盤(pán)已初始化")
loadFileCallback?.invoke(fs, fs.rootDirectory)
} catch (e: Exception) {
Log.e("UsbHelper", "USB 初始化異常: ${e.message}", e)
}
}
}
注意:
獲取u盤(pán)數(shù)據(jù)用的libaums 開(kāi)源庫(kù)
implementation 'me.jahnen.libaums:core:0.10.0'
3.文件Copy
將U盤(pán)識(shí)別和文件讀取分裝成一個(gè)兩個(gè)工具類.
3.1 盤(pán)識(shí)別工具類
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
// 待申請(qǐng)權(quán)限的設(shè)備集合,避免重復(fù)申請(qǐng)
private val pendingDeviceIds = mutableSetOf<Int>()
// 已初始化的設(shè)備集合,方便拔出處理
private val initializedDeviceIds = mutableSetOf<Int>()
// U盤(pán)文件加載回調(diào)
var loadFileCallback: ((fs: FileSystem, rootDir: UsbFile) -> Unit)? = null
// U盤(pán)拔出回調(diào)
var usbRemovedCallback: ((device: UsbDevice) -> Unit)? = null
// 廣播是否已注冊(cè)
private var isReceiverRegistered = false
/**
* 入口方法:處理 USB 權(quán)限和文件系統(tǒng)加載
*/
fun processUsb(context: Context, loadFileCallback: (fs: FileSystem, rootDir: UsbFile) -> Unit) {
if (isReceiverRegistered) return
this.loadFileCallback = loadFileCallback
val appContext = context.applicationContext
// 檢查設(shè)備是否支持 USB HOST
if (!appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
appContext.showToast("設(shè)備不支持 USB HOST")
return
}
// 注冊(cè)廣播接收器
usbPermissionReceiver = CustomUsbPermissionReceiver()
val filter = IntentFilter().apply {
addAction(MediaShowConstant.ACTION_USB_PERMISSION) // 自定義權(quán)限廣播
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) // U盤(pán)插入廣播
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) // U盤(pán)拔出廣播
}
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
// 檢查當(dāng)前已連接的 USB 設(shè)備
checkUsbDevices(appContext)
}
/**
* 檢查當(dāng)前已連接的 USB 設(shè)備并處理
*/
private fun checkUsbDevices(context: Context) {
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
for (device in usbManager.deviceList.values) {
if (!isMassStorageDevice(device)) continue // 只處理存儲(chǔ)類設(shè)備
if (!usbManager.hasPermission(device)) {
requestPermission(context, usbManager, device) // 請(qǐng)求權(quán)限
} else {
loadUsbFile(context, device) // 已有權(quán)限直接加載
}
}
}
/**
* 判斷設(shè)備是否為 USB 存儲(chǔ)類
*/
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 文件系統(tǒng)
*/
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 設(shè)備
// 只加載第一個(gè)分區(qū)(如需多分區(qū)可遍歷 partitions)
val fs = usbDevice.partitions[0].fileSystem
initializedDeviceIds.add(device.deviceId) // 標(biāo)記已初始化
Log.d("UsbHelper", "調(diào)用回調(diào),U盤(pán)已初始化")
loadFileCallback?.invoke(fs, fs.rootDirectory) // 回調(diào)文件系統(tǒng)
} 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()
}
/**
* 請(qǐng)求 USB 權(quán)限
*/
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", "申請(qǐng) U 盤(pán)權(quán)限: ${device.deviceName}")
usbManager.requestPermission(device, permissionIntent)
}, 200) // 延遲 200ms 避免廣播未注冊(cè)
}
/**
* 自定義廣播接收器
*/
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) // 插入時(shí)請(qǐng)求權(quán)限
}
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 權(quán)限已授權(quán),開(kāi)始加載文件系統(tǒng)")
loadUsbFile(context, device) // 權(quán)限允許后加載文件
} else {
Log.w("UsbHelper", "USB 權(quán)限被拒絕: ${device.deviceName}")
}
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
if (isMassStorageDevice(device) && initializedDeviceIds.remove(device.deviceId)) {
Log.d("UsbHelper", "U盤(pán)拔出,移除初始化標(biāo)記: ${device.deviceName}")
usbRemovedCallback?.invoke(device) // 回調(diào)拔出事件
}
}
}
}
}
}3.2 文件復(fù)制工具類
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"
/**
* 復(fù)制整個(gè) USB 目錄(遞歸方式),保證視頻和音樂(lè)文件都能正常復(fù)制
*/
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
// 確保目標(biāo)根目錄存在
if (!targetDir.exists() && !targetDir.mkdirs()) {
Log.e(TAG, "Failed to create targetDir: ${targetDir.absolutePath}")
return@withContext false
}
// 收集所有待復(fù)制文件
val fileList = mutableListOf<Pair<UsbFile, File>>()
collectUsbFiles(usbDir, targetDir, filter, fileList)
// 回調(diào)總數(shù)(在主線程)
Handler(Looper.getMainLooper()).post {
callback?.onStart(fileList.size)
}
var current = 0
var allSuccess = true
for ((source, target) in fileList) {
try {
// 創(chuàng)建父目錄
val parent = target.parentFile
if (parent != null && !parent.exists() && !parent.mkdirs()) {
Log.e(TAG, "Failed to create parent directory: ${parent.absolutePath}")
allSuccess = false
continue
}
// 跳過(guò)同名且大小相同的文件
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)
// 手動(dòng)循環(huán)讀取,確保所有文件(包括 MP3)都能完整寫(xiě)入
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)
}
}
}
// 完成回調(diào)
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 文件夾內(nèi)所有文件,保持目錄層級(jí)一致
*/
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 調(diào)用示例
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 的存讀取權(quán)限的申請(qǐng).
- 讀文件三方庫(kù)
- implementation 'me.jahnen.libaums:core:0.10.0'
總結(jié)
U盤(pán)數(shù)據(jù)讀取分為 配置,權(quán)限申請(qǐng) 獲取數(shù)據(jù) 復(fù)制文件幾步.這里總結(jié)了一下,因?yàn)槭情_(kāi)發(fā)板Android 14實(shí)現(xiàn),所以其他版本只能遇到了再處理。
以上就是Android外接U盤(pán)的操作實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于Android外接U盤(pán)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android多設(shè)備多module打包fat-aar(最新推薦)
這篇文章主要介紹了Android多設(shè)備多module打包(fat-aar),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
Android編程使用光線傳感器獲取光線強(qiáng)弱的方法【LightSensorManager封裝類】
這篇文章主要介紹了Android編程使用光線傳感器獲取光線強(qiáng)弱的方法,結(jié)合實(shí)例形式分析了Android光線傳感器管理封裝類LightSensorManager的具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-11-11
Android啟動(dòng)畫(huà)面的實(shí)現(xiàn)方法
這篇文章主要介紹了Android啟動(dòng)畫(huà)面的實(shí)現(xiàn)方法,分析了布局文件及加載啟動(dòng)文件的實(shí)現(xiàn)方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-01-01
Ubuntu中為Android實(shí)現(xiàn)Application Frameworks層增加硬件訪問(wèn)服務(wù)
本文主要介紹Android實(shí)現(xiàn) Application Frameworks層增加硬件訪問(wèn)服務(wù),這里對(duì)實(shí)現(xiàn)增加硬件訪問(wèn)服務(wù)的功能做出了詳細(xì)的工作流程,并提供示例代碼,有需要的小伙伴參考下2016-08-08
Android onbackpressed實(shí)現(xiàn)返回鍵的攔截和彈窗流程分析
很多網(wǎng)友不明白如何在Android平臺(tái)上捕獲Back鍵的事件,Back鍵是手機(jī)上的后退鍵,一般的軟件不捕獲相關(guān)信息可能導(dǎo)致你的程序被切換到后臺(tái),而回到桌面的尷尬情況,在Android上有兩種方法來(lái)獲取該按鈕的事件2023-01-01
Android ContentProvider的實(shí)現(xiàn)及簡(jiǎn)單實(shí)例代碼
這篇文章主要介紹了Android ContentProvider的實(shí)現(xiàn)及簡(jiǎn)單實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
Android利用GridView實(shí)現(xiàn)單選效果
本篇文章主要介紹了Android利用GridView實(shí)現(xiàn)單選效果的相關(guān)知識(shí),具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-05-05
Flutter List數(shù)組避免插入重復(fù)數(shù)據(jù)的實(shí)現(xiàn)
這篇文章主要介紹了Flutter List數(shù)組避免插入重復(fù)數(shù)據(jù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
Android自定義下拉刷新控件RefreshableView
這篇文章主要介紹了Android自定義下拉刷新控件RefreshableView,支持所有控件的下拉刷新,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11

