前言
最近需要實現一個 TV 或一體機從 U 盤讀取數據顯示的功能,該功能主要解決的問題是:
- 獲取 U 盤根目錄
- 解決拔出 U 盤進程被殺死的問題
獲取 U 盤根目錄
獲取 U 盤根目錄需要分兩種情況:
1. 應用程序已經在運行,這個時候插入 U 盤。
這種情況我是通過監聽媒體掛載的廣播來實現的,具體代碼如下:
註冊廣播:
<receiver
android:name=".USBBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED"/>
<action android:name="android.intent.action.MEDIA_UNMOUNTED"/>
<action android:name="android.intent.action.MEDIA_EJECT"/>
<data android:scheme="file"/>
</intent-filter>
</receiver>
監聽 U 盤插入廣播並獲取 U 盤根目錄:
public class USBBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
switch (intent.getAction()) {
case Intent.ACTION_MEDIA_MOUNTED://擴展介質被插入,而且已經被掛載。
if (intent.getData() != null) {
String path = intent.getData().getPath();
String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(path);
}
break;
}
}
}
經測試,intent.getData().getPath(); 在一體機上獲取的並不是 U 盤最終的根目錄,所以通過 getUSBRealRootDirectory() 方法再一次提取最終的根目錄,該方法具體如下:
/**
* 獲取 U 盤真正根目錄
*
* @param usbTempRootDirectory U 盤臨時根目錄
* @return U 盤真正根目錄
*/
public static String getUSBRealRootDirectory(String usbTempRootDirectory) {
String realUSBRootDirectory = "";
File dir = new File(usbTempRootDirectory);
File[] files = dir.listFiles();
/**
* 注意:
* 經測試,
* TV 直接是 usbTempRootDirectory 作爲 U 盤的根目錄,例如:/storage/577F-85CA
* 一體機會在 U 盤的根目錄(usbTempRootDirectory=/mnt/usb_storage/USB_DISK4)下再創建多個包含 "udisk" 的目錄,然後其中一個作爲 U 盤的根目錄,例如:/mnt/usb_storage/USB_DISK4/udisk0
*/
if (files != null) {
for (File file : files) {
//如果根目錄下還有包含 "udisk" 的目錄,則該包含 "udisk" 的目錄纔是 U 盤真正的根目錄
if (file.isDirectory() && file.list().length > 0 && file.getAbsolutePath().contains("udisk")) {
realUSBRootDirectory = file.getAbsolutePath();
break;
} else { // 如果根目錄下沒有包含 "udisk" 的目錄,說明 dir 就是根目錄
realUSBRootDirectory = dir.getAbsolutePath();
}
}
}
return realUSBRootDirectory;
}
2. 應用程序還未運行,U 盤就已經插入了。
這種情況就無法通過監聽廣播拿到 U 盤根目錄了,經查詢也沒找到特定 API 可以獲取到,所以這裏只能用反射的方法。具體如下:
通過反射方法獲取 U 盤臨時根目錄
/**
* 獲取 U 盤臨時根目錄(一體機會在臨時目錄下再創建多個包含 "udisk" 的目錄,所以臨時目錄並不是 U 盤真正的根目錄)
*
* @param context Context
* @return U 盤臨時根目錄集合
*/
public static List<String> getUSBTempRootDirectory(Context context) {
List<String> usbTempRootDirectory = new ArrayList<>();
try {
StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<StorageManager> storageManagerClass = StorageManager.class;
String[] paths = (String[]) storageManagerClass.getMethod("getVolumePaths").invoke(storageManager);
for (String path : paths) {
Object volumeState = storageManagerClass.getMethod("getVolumeState", String.class).invoke(storageManager, path);
//路勁包含 internal 一般是內部存儲,例如 /mnt/internal_sd,需要排除
if (!path.contains("emulated") && !path.contains("internal") && Environment.MEDIA_MOUNTED.equals(volumeState)) {
usbTempRootDirectory.add(path);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return usbTempRootDirectory;
}
同樣,在一體機上獲取的並不是 U 盤最終的根目錄,所以還是通過 getUSBRealRootDirectory() 方法再一次提取最終的根目錄,具體如下:
List<String> usbTempRootDirectory = FileUtils.getUSBTempRootDirectory(this);
for (int i = 0; i < usbTempRootDirectory.size(); i++) {
String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(usbTempRootDirectory.get(i));
}
解決拔出 U 盤進程被殺死的問題
因爲需要從 U 盤獲取視頻地址進行播放,當正在播放的時候拔出 U 盤就會出現進程被殺死的情況,報錯日誌如下:
ProcessKiller: Process com.xxx.xxx (2088) has open file /mnt/usb_storage/USB_DISK4/udisk0/xxx.mp4
ProcessKiller: Sending SIGHUP to process 2088
Vold: Failed to unmount /mnt/usb_storage/USB_DISK4/udisk0 (Device or resource busy, retries 1, action 2)
ActivityManagerService: Process com.xxx.xxx (pid 2088) has died
這是因爲拔出 U 盤的時候,視頻資源被視頻播放器佔用所導致的。可是我明明是做了拔出處理的,即在收到 U 盤被拔出的廣播後釋放視頻資源,如下:
public class USBBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
switch (intent.getAction()) {
case Intent.ACTION_MEDIA_UNMOUNTED://擴展介質存在,但是還沒有被掛載。(擴展介質已被拔出)
//這裏釋放所有佔用的資源
break;
}
}
}
後來 debug 發現,其實在還未收到 U 盤被拔出的廣播,進程就被殺死了。。。
既然不能在監聽到 U 盤拔出的時候釋放播放資源,那就只能換一種方法了。最後想到的方法是將播放視頻的 activity 單獨放到一個進程,這樣即使該進程被殺死,也不會影響到整個應用奔潰。
雖然通過上面的方法解決了整個應用奔潰的問題,但是還是覺得不完美,總覺得 Android 不可能只提供了 U 盤拔出後的廣播,而沒有提供 U 盤將要被拔出的廣播呀!經過一番查找,嗯,真香!確實有這個廣播-android.intent.action.MEDIA_EJECT,該廣播表示用戶想要移除擴展介質,即擴展介質將要被拔出。收到這個廣播釋放佔用的資源即可,例如視頻播放器釋放視頻資源,文本讀寫需要關閉流等等。
完整的廣播監聽如下:
public class USBBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
switch (intent.getAction()) {
case Intent.ACTION_MEDIA_MOUNTED://擴展介質被插入,而且已經被掛載。
if (intent.getData() != null) {
String path = intent.getData().getPath();
String usbRealRootDirectory = FileUtils.getUSBRealRootDirectory(path);
}
break;
case Intent.ACTION_MEDIA_EJECT://用戶想要移除擴展介質(擴展介質將要被拔出)
//這裏釋放所有佔用的資源
break;
case Intent.ACTION_MEDIA_UNMOUNTED://擴展介質存在,但是還沒有被掛載。(擴展介質已被拔出)
//這裏做一些拔出 U 盤後的其他操作
break;
}
}
}
以上就是 Android 設備與 U 盤之間的交互知識,關於獲取 U 盤根目錄,如果你有更好的方法歡迎交流~
相關源碼:AndroidUSB