Android 設備與 U 盤之間的交互

前言

最近需要實現一個 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章