androidOTG (USB读写,U盘读写) 最全使用相关总结
- 简介
- 第一种读取方法:android推荐使用的通过endpoint的形式进行通信
- 第二种读取方法:像读你sdcard的形式来读你的U盘设备
- 注意注意注意
提示
博主:来自火星的萨满_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的时候,需要检测这个设备有没有操作权限,如果没有操作权限怎么办,那就要申请啊。其实申请和反馈其实就是靠这个广播来进行反馈的。所以在注册时候一般都是有三个广播类型
动态注册的代码如下所示:
@Overridepublic 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>() {//简单排序 文件夹在前 文件在后@Overridepublic 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上这个开源的库
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文件是不是也是这种形式??答案当然是可以找到的啊。
截图如下:
在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
参考的blog引用:
https://blog.csdn.net/elsa_rong/article/details/47005129
以上的知识点是自己参考了部分博客以及源码的小小总结,喜欢的朋友点波关注。
欢迎持续访问博客