1. 程式人生 > >android OTG (USB讀寫,U盤讀寫)最全使用相關總結

android OTG (USB讀寫,U盤讀寫)最全使用相關總結

androidOTG (USB讀寫,U盤讀寫) 最全使用相關總結


  1. 簡介
  2. 第一種讀取方法:android推薦使用的通過endpoint的形式進行通訊
  3. 第二種讀取方法:像讀你sdcard的形式來讀你的U盤裝置
  4. 注意注意注意

簡介

  首先關於現在android裝置,乃至很多硬體裝置其實都在需要接入otg相關的功能,即類似通過USB介面的形式,接入外接的裝置,如接入外接的攝像頭,或者接入U盤等等硬體裝置,當然在實際的使用中,在實際開發過程中,往往接入U盤的通用性上會大很多。所以在開發的時候也就面臨著怎麼去讀取外接裝置,當然如果你接入的是U盤與接入攝像頭這樣的裝置還是有區別的,本文僅僅是以外接U盤的形式來講解的。 我使用的android版本是android6.0的版本

第一種讀取方法:android推薦使用的通過endpoint的形式進行通訊

  簡單講一下這種方式的優點和通用性,首先這種方式肯定是最優,最通用的方式,無論你針對的是什麼樣的裝置,我都可以通過endPoint的形式,通過雙向fifo管道的來進行通訊,無論你是磁碟裝置,乃至你是攝像頭,或者其它硬體裝置等等,只要涉及到通訊,我都可以使用這種方式來進行讀寫

OTG涉及到的相關的android中的類

  Otg從新增後,一直可能算是一個相對來說稍微比較難的一點地方吧,因為磁碟或者linux本身機制的形式,從事上層開發的,對其底層的通訊機制等等不是特別的清楚,導致其在理解上可能會稍微難一點。也不知道EndPoint等等一些到底是幹嘛的。簡單扯點linux下的東西,沒仔細研究過,之前看了一篇部落格說的,協議不說,只說機制:底層使用的是雙向命名管道形式,endPoint其實也就是類似檔案描述符,在linux底層通訊中,一個檔案對應一個fd,即檔案描述符,linux下萬物皆檔案,所有如果你想操作檔案,必須拿到fd。你可以想象一下,通訊是雙向的,即傳送方和接收方,所有它需要兩個檔案描述符來處理啊。Java上層在其基礎之上做了很好的封裝。

下面簡單瞭解下在OTG裡面經常使用到的一些類:
1:UsbManager.java 不用多說,對應的是Usb的管理類
2:UsbDevice.java Usb裝置繫結上,肯定要拿到對應的裝置吧
3:UsbInterface.java Usb對應的介面,通過介面拿到內部匹配Usbpoint
4:UsbEndPoint.java Usb通訊資料的傳輸主要其實就是通過這個類來進行的
5:UsbDeviceConnection Usb聯結器

  打個比方,其實整個通訊就相當於hdmi線一樣,一遍接著電腦,一遍接著另外的顯示器。即線頭部分叫做UsbInterface,線頭裡面對應的線子口對應了UsbEndPoint,你要建立連線才能通訊吧,那就是UsbDeviceConnection了,那有人說UsbDevice像什麼。在連線的時候,該有個主從區分吧。到底是你連我的,還是我連你的,你是主機還是我是主機,被連線的那個就是UsbDevice了。你連線我,我主要來負責操作,你負責相應就完了。當然,這個其實是我自己的個人理解。可能有差異的地方。還是斧正。


OTG廣播的監聽

  在將OTG廣播之前,不得不講一下廣播的靜態註冊和動態註冊形式。做android的你要是不知道廣播的註冊形式,那我估計你的android也可以gg了。

首先講一個OTG的廣播型別吧

在android6.0中,只有兩個

1:public static final String ACTION_USB_DEVICE_ATTACHED ="android.hardware.usb.action.USB_DEVICE_ATTACHED";  //對應的是USB裝置插入時候的廣播
2: public static final String ACTION_USB_DEVICE_DETACHED ="android.hardware.usb.action.USB_DEVICE_DETACHED";  //對應的USB裝置拔出的時候的廣播
很多人會有疑問,為什麼我在那麼多demo中看到好像還有一個廣播啊,沒錯,其實還有一個廣播,不過這個廣播是我們自己的定義的
3:private static final String ACTION_USB_PERMISSION = "com.android.usb.USB_PERMISSION";
注意:

這個廣播是自己定義的。在attached的時候,需要檢測這個裝置有沒有操作許可權,如果沒有操作許可權怎麼辦,那就要申請啊。其實申請和反饋其實就是靠這個廣播來進行反饋的。所以在註冊時候一般都是有三個廣播型別

動態註冊的程式碼如下所示:

@Override
    public void registerReceiver() {
        IntentFilter mUsbDeviceFilter = new IntentFilter();
        mUsbDeviceFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        mUsbDeviceFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        mUsbDeviceFilter.addAction(ACTION_USB_PERMISSION);
        mContext.registerReceiver(this,mUsbDeviceFilter);
    }

去靜態的註冊廣播

靜態註冊廣播有個需要主要的地方,就是廣播必須要有一個預設的構造引數,大概原始碼在使用的時候,通過反射或者別的形式,必須要呼叫一下它這個靜態的構造方法吧

<receiver android:name="com.receiver.OtgReceiver">
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
                <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
            </intent-filter>
     </receiver>
不多談,就簡單明瞭

能不能在Activity上註冊這樣的一個廣播呢??收到某一些U盤插入的直接啟動??

  答案當然是可以的。上次在原始碼裡面看到關於這一塊的東西。確實是可以的,而且貌似只針對這一個廣播,其它型別的廣播我還沒復現這種情況。這種應用主要用於什麼場景???工程U盤型別,只有指定U盤插入的時候才能夠將介面啟動起來。 表現形式如下所示:

<activity android:name="com.receiver.Usb">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>

看到了吧,需要在intent-filter中註冊一個靜態的ATTACHED的action,然後而且還多了一個meta,注意哈。這個標籤元素,這種使用方式在原始碼的Gallery2裡面有表現。
device_filter.xml是自己建立的一個xml檔案,裡面規定了U盤的某些引數:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Sentinel HL Driverless VendorID=0x0529 ProductId=0x0003 -->
    <usb-device vendor-id="13212" product-id="38" />
    <usb-device vendor-id="17" product-id="30600"/>
</resources>

如上所示:規定了usb-device vendor-id,即廠商的id,以及產品的id,當然如果你僅僅寫一個引數
如你只寫vendor-id也是可以的。這種形式其實就是告訴你這個vendor-id的所有裝置其實都是可以喚醒裝置的

OTG的許可權

差點就漏了,otg也是對外接裝置的讀寫操作啊:
如下所示許可權:

<uses-feature android:name="android.hardware.usb.host" />    //表示支援usb裝置
    <!-- Permission required to access the external storage for storing and loading files -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

OTG裝置的讀取方式

在廣播attached的時候,也可以去獲取USB裝置

廣播註冊完畢之後,當Usb插拔的時候,其實就會把對應的Usb裝置傳送過來。

UsbDevice usbdevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

注意注意注意:外接OTG裝置在插入的時候,就會發送這個廣播,廣播發送之後,還會做一個初始化和掛載的過程,大概在2-3s左右,如果你還在它沒準備好的時候就去讀寫,那麼肯定是不行的。經測,確實不行

USB的讀取方式即讀取Usb裝置,靜態的時候讀取,即已經在插入穩定之後,再去做讀裝置操作

public HashMap<String,UsbDevice> readDeviceList() {
        UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
        HashMap<String,UsbDevice> mDevices =  usbManager.getDeviceList();
        mPendingIntent =PendingIntent.getBroadcast(mContext,0,new Intent(ACTION_USB_PERMISSION),0);
        if (null != mDevices && mDevices.size() != 0){
            Iterator<UsbDevice> iterator = mDevices.values().iterator();
            while (iterator.hasNext()){
                UsbDevice usb = iterator.next();
                if (!usbManager.hasPermission(usb)){
                    Log.i(TAG,"has not permission");
                    usbManager.requestPermission(usb,mPendingIntent);
                }else {
                    Log.i(TAG,"has permission");
                }
            }
        }
        return mDevices;
}

拿到所有的UsbDevice裝置之後,就可以去做,就可以繼續在往下操作了。

OTG(USB HOS)的整個操作流程如下所示:

通訊主要就是呼叫這些API:UsbManager->UsbDevice->UsbInterface->UsbEndpoint->UsbDeviceConnection按照一步一步來,在後面會講,也可以參考別的

OTG裝置的通訊方式

OTG裝置的整個操作過程如上述流程所示:
通過程式碼測試,發現我的OTG裝置有2個Interface,其中Interface-0有一個Endpoint-0(輸入);Interface-1有兩個Endpoint(其中Endpoint-0是輸入,Endpoint-1是輸出)。故在程式碼中呼叫Interface-1實現資料收發,注意選擇使用有兩個EndPoint的進行雙向通訊

程式碼如下所示:

UsbInterface usbInterface = usbDevice.getInterface(1);
    UsbEndpoint inEndpoint = usbInterface.getEndpoint(0);
    UsbEndpoint outEndpoint = usbInterface.getEndpoint(1);
    UsbDeviceConnection connection = usbManager.openDevice(usbDevice);
    connection.claimInterface(usbInterface, true);

    sendStringMsg = "0x88";
    sendBytes = HexString2Bytes(sendStringMsg);            
    int out = connection.bulkTransfer(outEndpoint, sendBytes, sendBytes.length, 5000);
    displayToast("傳送:"+out+" # "+sendString+" # "+sendBytes);

    receiveMsgBytes = new byte[32];
    int in = connection.bulkTransfer(inEndpoint, receiveMsgBytes, receiveBytes.length, 10000);
    receiveMsgString = Bytes2HexString(receiveMsgBytes);
    displayToast("應答:"+in+" # "+ receiveMsgString +" # "+receiveBytes.length);

執行步驟:
1:先通過usbDevice來獲取UsbInterface
2:然後通過UsbInterface來獲取UsbEndPoint型別,注意對應的收發的檔案描述符
3:開啟裝置usbManager.openDevice(usbDevice);獲取連線型別
4:建立連線與介面之間的關係connection.claimInterface(usbInterface, true);
5:通過connection來發送資料型別

注意點:
1:USB通訊協議中有4中傳輸模式,分別是:
Bulk Transaction
Interrupt Transaction
Control Transation
Isochronous Transaction
我採用了Bulk Transaction大塊資料的傳輸模式,關於這一部分的瞭解可以參考USB通訊協議,這幾種都是可以使用的。具體可以自己去搜一下關於這幾個的區別

已知的關於OTG通訊的封裝類庫

以上的形式如果是自己寫的話,是可以通過這種方式來完成OTG的通訊的,但是已經有牛人為我們封好了庫libaums

依賴方式

compile 'com.github.mjdev:libaums:+'

庫中比較重要的類:

UsbMassStorageDevice 外接的Usb儲存裝置
FileSystem 外接的檔案系統
UsbFile 外接的檔案型別
。。。還有很多

簡單使用

讀取裝置列表

public void readDeviceList() {
        UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
        returnMsg("開始去讀Otg裝置");
        storageDevices = UsbMassStorageDevice.getMassStorageDevices(mContext);
        mPendingIntent =PendingIntent.getBroadcast(mContext,0,new Intent(ACTION_USB_PERMISSION),0);
        if (storageDevices.length == 0) {
            returnMsg("沒有檢測到U盤s");
            return;
        }
        for (UsbMassStorageDevice device : storageDevices){
            if (usbManager.hasPermission(device.getUsbDevice())){
                returnMsg("檢測到有許可權,延遲1秒開始讀取....");
                try {
                    Thread.sleep(1000 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                readDevice(device);
            }else {
                returnMsg("檢測到有裝置,但是沒有許可權,申請許可權....");
                usbManager.requestPermission(device.getUsbDevice(),mPendingIntent);
            }
        }
    }

讀取檔案:

private void readFile(UsbFile root){
        ArrayList<UsbFile> mUsbFiles = new ArrayList<>();
        try {
            for (UsbFile file: root.listFiles()){
                Log.i(TAG,file.getName());
                mUsbFiles.add(file);
            }
            Collections.sort(mUsbFiles, new Comparator<UsbFile>() {//簡單排序 資料夾在前 檔案在後
                @Override
                public int compare(UsbFile oFile1, UsbFile oFile2) {
                    if (oFile1.isDirectory()) return -1;
                    else return 1;
                }
            });
            if (broadcastListener !=null){
                broadcastListener.updateUsbFile(mUsbFiles);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
}

讀取裝置檔案資訊:

private void readDevice(UsbMassStorageDevice device) {
        try {
            device.init();
            Partition partition = device.getPartitions().get(0);
            Log.i(TAG,"------------partition---------");
            Log.i(TAG,"VolumnLobel:"+partition.getVolumeLabel());
            Log.i(TAG,"blockSize:"+partition.getBlockSize()+"");
            FileSystem currentFs = partition.getFileSystem();
            Log.i(TAG,"------------FileSystem---------");
            UsbFile root = currentFs.getRootDirectory();
            String deviceName = currentFs.getVolumeLabel();
            Log.i(TAG,"volumnLable:"+deviceName);
            Log.i(TAG,"chunkSize:"+currentFs.getChunkSize());
            Log.i(TAG,"freeSize:"+currentFs.getFreeSpace());
            Log.i(TAG,"OccupiedSpcace:"+currentFs.getOccupiedSpace());
            Log.i(TAG,"capacity"+currentFs.getCapacity());
            Log.i(TAG,"rootFile:"+root.toString());
            returnMsg("正在讀取U盤" + deviceName);
            readFile(root);
        } catch (IOException e) {
            e.printStackTrace();
            returnMsg("讀取失敗:"+e.getMessage());
        }finally {
        }
    }

以上都是在測試的時候的程式碼:注意注意:操作完了之後需要關閉:

for (UsbMassStorageDevice s:storageDevices){
                if (s.getUsbDevice() == mUsbDeviceRemove)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        s.getPartitions().stream().close();
                }
            }

這裡面其實有一個問題,如果快速插拔U盤的時候,會出現異常資訊的。所有需要在讀取的時候新增一個延遲操作。具體細節性的使用,感興趣的可以參考下github上這個開源的庫

第二種讀取方法:像讀你sdcard的形式來讀你的U盤裝置

現在來講一下第二種方式,有做過sd卡方面開發經驗的,肯定都知道,sd卡的讀寫直接是可以檢測到sd是否mounted的,並且直接可以操作sd卡上的檔案,而不是通過上述的通訊方式進行開發。Linux核心下的宗旨就是萬物皆檔案,無論你是裝置檔案,管道檔案,還是磁碟檔案,等等。所有的都是檔案型別,那麼我們也就能找到這個檔案吧。
android系統在早期設計的時候,很多時候設計師有點不太合理的,後期會做修正,但是會不會把以前的完全給幹掉???沒辦法啊,因為要向前相容啊,如果幹掉之後,那前面的怎麼玩???舉個例子,很久以前內建sd卡的路徑/mnt/sdcard/ 現在依然存在,你可以直接對它操作,
外接sd卡也是/mnt/sdcard1,多個的時候繼續往下加就行了。只是android在後期升級的時候,給這個路徑加了個軟連線,指向了另外一個路徑。其實無論是操作這個路徑還是操作其它軟連線的路徑其實都是一回事。
那麼otg檔案是不是也是這種形式??答案當然是可以找到的啊。
截圖如下:
這裡寫圖片描述

在mnt檔案下,當otg插入的時候,會產生一個udisk檔案,並且這個udisk指向了/mnt/media_rw/BD71-01F1這個檔案,所以其實這兩個檔案其實是一個意思。不要做什麼區分。

所以我們能不能直接對它進行操作啊???當然是可以的啊。當U盤attach上的其實就可以去操作啦。直接讀寫。

讀寫方式

private static final String MEDIA_PATH = "/mnt/udisk";
private static final String MEDIA_FOLDER = "media";

private void traverseFolder(String path) {
            File file = new File(path);
            if (file.exists()) {
                File[] files = file.listFiles();
                if (files.length == 0) {
                    Log.i(TAG,file.getAbsolutePath()+"\t"+"is null");
                    return;
                } else {
                    for (File file2 : files) {
                        if (file2.isDirectory()) {
                            Log.i(TAG,"資料夾:" + file2.getAbsolutePath());
                            traverseFolder(file2.getAbsolutePath());
                        } else {
                            mArrayList.add(file2);
                        }
                    }
                }
            } else {
                Log.i(TAG,"資料夾不存在");
            }
        }

程式碼奉上,具體測試就暫時不測了

異常重要的判斷方式(強烈注意)

ACTION_USB_DEVICE_ATTACHED和ACTION_USB_DEVICE_DETACHED的缺點

這兩個廣播是去監測U盤插入和拔出的,也就意味著,你只要一插入或者一拔出U盤,就是收到這兩個廣播。它不會管你的裝置有沒有準備好,有沒有mounted或者unmounted。用腳指頭想象,如果它都沒準備好,你能不能對它進行讀寫啊。顯然是不能的吧。所以所有的操作必須要在它準備好之後才能做吧。前兩天在原始碼裡面low了一眼,結果看到相關的解決方式

VolumeInfo和DiskInfo的引入,外接裝置Mounted和UnMounted廣播的引入

在官方androidSDK中,VolumeInfo和DiskInfo類都是被hide掉了,也就意味著其實你用不了,並且其廣播也沒有具體的靜態常量來引入,但是它的廣播系統應用能收到,那我們做的應用也能不能收到,它沒有寫的一些常量,我們能不能把它單獨提出來自己寫一套嘞。可能沒有系統完善,但是也能做到吧。

認識廣播android.os.storage.extra.VOLUME_STATE

沒錯,其實就是這個廣播。這個廣播就是用來監聽Volume狀態的。通過監聽它來檢視我們的,當外接Usb裝置在Mounted或者UnMounted的時候則就可以用來做監聽。只是它是一個狀態變換的。
註冊廣播

這裡寫圖片描述

監聽操作:

這裡寫圖片描述

注意注意:在這個監聽下,如果你通過

這裡寫圖片描述
獲取Usb裝置的話,返回的是null啊,所以在這裡不能對usbDevice進行操作

肯定還有人問,VolumeInfo類不是被hide掉了麼,你這裡怎麼有,問這個的,我真的很像錘死你。不能把裡面的狀態扣出來,自己寫一個VolumeInfo啊

這裡寫圖片描述
這不就行了啊。

注意注意注意:

許可權問題,許可權問題,許可權問題。重要的問題說3遍
Android 6.0引入了動態的許可權,一定要檢查你的應用有沒有,如果沒有讀寫許可權的話,
android.os.storage.extra.VOLUME_STATE這個廣播是收不到的

這裡寫圖片描述

如果沒有記得先在Manifest.xml中註冊,然後去動態申請。
關於如何動態申請許可權的操作。哥就不贅述了

有興趣的可以看看原始碼所在地

/base/services/core/java/com/android/server/MountService.java
/base/core/java/android/os/storage/VolumeInfo.java
/base/core/java/android/os/storage/StorageManager.java

系統程式碼的案例:
/TvSettings/Settings/src/com/android/tv/settings/device/storage/NewStorageActivity.java

以上的知識點是自己參考了部分部落格以及原始碼的小小總結,喜歡的朋友點波關注。