天天看點

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

Android USB OTG U盤讀寫相關使用最全總結

https://blog.csdn.net/qq_29924041/article/details/80141514

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

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

提示 

部落客:來自火星的薩滿_906285288 

部落格位址: https://blog.csdn.net/qq_29924041 

轉載請注明出處

簡介

  首先關于現在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);
    }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

去靜态的注冊廣播

靜态注冊廣播有個需要主要的地方,就是廣播必須要有一個預設的構造參數,大概源碼在使用的時候,通過反射或者别的形式,必須要調用一下它這個靜态的構造方法吧

<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>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

不多談,就簡單明了

能不能在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>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

看到了吧,需要在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>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如上所示:規定了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" />
           
  • 1
  • 2
  • 3
  • 4

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;
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

拿到所有的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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

執行步驟: 

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:+'
           
  • 1

庫中比較重要的類:

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);
            }
        }
    }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

讀取檔案:

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();
        }
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

讀取裝置檔案資訊:

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 {
        }
    }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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

for (UsbMassStorageDevice s:storageDevices){
                if (s.getUsbDevice() == mUsbDeviceRemove)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        s.getPartitions().stream().close();
                }
            }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

https://github.com/magnusja/libaums#using-buffered-streams-for-more-efficency

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

現在來講一下第二種方式,有做過sd卡方面開發經驗的,肯定都知道,sd卡的讀寫直接是可以檢測到sd是否mounted的,并且直接可以操作sd卡上的檔案,而不是通過上述的通信方式進行開發。Linux核心下的宗旨就是萬物皆檔案,無論你是裝置檔案,管道檔案,還是磁盤檔案,等等。所有的都是檔案類型,那麼我們也就能找到這個檔案吧。 

android系統在早期設計的時候,很多時候設計師有點不太合理的,後期會做修正,但是會不會把以前的完全給幹掉???沒辦法啊,因為要向前相容啊,如果幹掉之後,那前面的怎麼玩???舉個例子,很久以前内置sd卡的路徑/mnt/sdcard/ 現在依然存在,你可以直接對它操作, 

外置sd卡也是/mnt/sdcard1,多個的時候繼續往下加就行了。隻是android在後期更新的時候,給這個路徑加了個軟連接配接,指向了另外一個路徑。其實無論是操作這個路徑還是操作其它軟連接配接的路徑其實都是一回事。 

那麼otg檔案是不是也是這種形式??答案當然是可以找到的啊。 

截圖如下: 

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

在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,"檔案夾不存在");
            }
        }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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

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

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的時候則就可以用來做監聽。隻是它是一個狀态變換的。 

注冊廣播

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

監聽操作:

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

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

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

擷取Usb裝置的話,傳回的是null啊,是以在這裡不能對usbDevice進行操作

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

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

這不就行了啊。

注意注意注意:

權限問題,權限問題,權限問題。重要的問題說3遍 

Android 6.0引入了動态的權限,一定要檢查你的應用有沒有,如果沒有讀寫權限的話, 

android.os.storage.extra.VOLUME_STATE這個廣播是收不到的

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

如果沒有記得先在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

參考的blog引用: 

https://blog.csdn.net/elsa_rong/article/details/47005129

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

歡迎持續通路部落格