安卓设备通过USB与外设通信有两种形式(无需ROOT):
- 与设备之间相互发送命令:用串口通信比较多,建议在github搜索felHR85,使用串口通信的前提是,外设支持串口通信且有串口通信的协议,外设不是单纯的存储设备
- 只读取设备的文件,有时需要删除文件:
- 方法一:通过USB挂载外设到文件系统,然后获取USB权限,把USB设备映射为存储设备,可以在GitHub上搜索libaums框架或者Usb Mass Storage for Android框架,前者是收藏最多的USB存储框架,后者是比较简单好用的框架。两者的实现原理相同。
- 方法二:有的USB设备不能完成上面两个框架所必须的初始化,比如我在初始化时遇到了claim interface failed的报错,这个报错是由native方法:native_claim_interface(int interfaceID, boolean force)抛出。从网上是找不到解决方案的,估计是设备不兼容。这时候直接打开文件管理,看USB设备是不是能在文件管理中直接看到,如果可以,我们可以认为它其实已经挂载了,所以libaums框架或者Usb Mass Storage for Android框架可能并不适用。我们直接把它当成SD卡的文件进行读取,实际上也支持删除文件。
下面介绍方法一和方法二的使用
如果你尝试了方法一行不通,可以转为使用方法二。两者都需要注册一个广播监听USB的插入和拔出,方法一还需要监听USB权限的获取结果:
public class UsbReceiver extends BroadcastReceiver {
private static final String TAG = "UsbReceiver";
public static final String ACTION_USB_PERMISSION = "ACTION_USB_PERMISSION";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
Log.d(TAG, "onReceive: USB_DEVICE_ATTACHED");
break;
case UsbManager.ACTION_USB_DEVICE_DETACHED:
Log.d(TAG, "onReceive: USB_DEVICE_DETACHED");
break;
case ACTION_USB_PERMISSION://方法二不需要
Log.d(TAG, "onReceive: ACTION_USB_PERMISSION");
break;
}
}
}
注册这个广播:
private void initUsbReceiver() {
mUsbReceiver = new UsbReceiver();
//注册广播,监听USB插入和拔出
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
intentFilter.addAction(UsbReceiver.ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mUsbReceiver);
}
这是动态注册广播的方式,不需要在manifest文件再注册了。
方法一详细使用:
1.申请存取权限:
- 1.github搜索并集成上述两种框架的一种
- 在manifest中加入:
<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-feature android:name="android.hardware.usb.host" />
<uses-permission
android:name="android.hardware.usb.host"
android:required="true" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
USB_PERMISSION就是后续需要在代码中动态请求的USB权限,我们知道现在安卓设备基本都需要动态请求权限了。
android:required="true"的意思是,如果安卓设备不支持USB权限,APP直接不能安装在这个设备上。
public void checkPermission() {
List<String> ps = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ps.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ps.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (ps.size() > 0) {
String[] pers = new String[ps.size()];
for (int i = 0; i < ps.size(); i++) {
pers[i] = ps.get(i);
}
ActivityCompat.requestPermissions(this, pers, REQUEST_PERMISSION_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CODE) {
for (int i = 0; i < permissions.length; i++) {
Log.i("MainActivity", "申请的权限为:" + permissions[i] + ",申请结果:" + grantResults[i]);
}
} else {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else {
Toast.makeText(this, "需要读写权限来保存并上传数据!", Toast.LENGTH_SHORT).show();
}
}
}
2.USB读取和使用
//三个全局变量
private UsbDevice mDevice;
private UsbDeviceConnection mConnection;
private PendingIntent mPermissionIntent;
//初始化
private void init() {
mUsbManager = (UsbManager) getContext().getSystemService(Context.USB_SERVICE);
mPermissionIntent = PendingIntent.getBroadcast(getContext(), 0, new Intent(UsbReceiver.ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
}
//请求权限和读取,使用
private void test() {
try {
HashMap<String, UsbDevice> list = mUsbManager.getDeviceList();
Iterator<UsbDevice> iterator = list.values().iterator();
while (iterator.hasNext()) {
mDevice = iterator.next();
// 申请USB权限,权限申请成功后,会收到上面注册的广播,然后需再调用test()方法
if (!mUsbManager.hasPermission(mDevice)) {
mUsbManager.requestPermission(mDevice, mPermissionIntent);
return;
}
}
//Usb Mass Storage for Android框架举例
mConnection = mUsbManager.openDevice(mDevice);
VirtualFileSystem fileSystem = new VirtualFileSystem(mDevice, mConnection);
if (fileSystem.mount(0)) { //这里如果报错,可能要考虑更换方法二
Toast.makeText(getContext(), "挂载成功", Toast.LENGTH_LONG).show();
}
for (VFSFile file : fileSystem.listFiles()) {
Log.d(TAG, "test: file : " + file);
//在这里对文件进行操作
}
} catch (Exception e) {
Log.d(TAG, "test: " + e.getMessage());
}
}
方法二使用详解:
如果方法一行不通,看插上USB设备后,能否在文件管理中看到,如果可以的话,直接把USB设备当着SD卡,用方法二进行操作:
1.申请存取权限:
在manifest中加入:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
动态申请权限:
public void checkPermission() {
List<String> ps = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ps.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ps.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (ps.size() > 0) {
String[] pers = new String[ps.size()];
for (int i = 0; i < ps.size(); i++) {
pers[i] = ps.get(i);
}
ActivityCompat.requestPermissions(this, pers, REQUEST_PERMISSION_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CODE) {
for (int i = 0; i < permissions.length; i++) {
Log.i("MainActivity", "申请的权限为:" + permissions[i] + ",申请结果:" + grantResults[i]);
}
} else {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else {
Toast.makeText(this, "需要读写权限来保存并上传数据!", Toast.LENGTH_SHORT).show();
}
}
}
2.开始文件操作
安卓的文件操作要求越来越严格,我建议不要使用添加
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
或者修改targetsdk = 29等骚操作来规避安卓文件安全要求,这是种迟早过时的办法,有的手机也已经没办法规避了。
建议使用安卓官方推荐的SAF框架进行文件存取:
SAF包含一个文档提供程序(Document provider) ,这个文档提供程序包含了USB:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-twecfElt-1660053575217)(https://upload-images.jianshu.io/upload_images/27762813-90cabc4bffd1c9bc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
让我们能够跳转到文档选择界面,选择自己要操作的文档,然后返回它的URI,并通过ContentResolver对其进行操作。
选择文件:
private static final int ACTIVITY_CHOOSE_FILE = 689;
public void onBrowse() {
Intent chooseFile;
chooseFile = new Intent(Intent.ACTION_OPEN_DOCUMENT);
//chooseFile = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); //选择文件夹
chooseFile.addCategory(Intent.CATEGORY_OPENABLE);
//设置自己所需文件的MIME类型
chooseFile.setType("application/octet-stream");
startActivityForResult(chooseFile, ACTIVITY_CHOOSE_FILE);
}
操作文件
@RequiresApi(api = Build.VERSION_CODES.Q)
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != RESULT_OK) return;
String path = "";
if (requestCode == ACTIVITY_CHOOSE_FILE) {
Uri uri = data.getData();
String fileName = uri.getPath().split(":")[1];
File file = uriToFileApiQ(uri, fileName, getContext());
if (file.exists()) {
Log.d(TAG, "onActivityResult: file exists");
}
System.out.print("Path = " + file);
}
}
//URI转File
@RequiresApi(api = Build.VERSION_CODES.Q)
public static File uriToFileApiQ(Uri uri, String fileName, Context context) {
File file = null;
if (uri == null) return file;
//android10以上转换
if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
file = new File(uri.getPath());
} else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
//把文件复制到沙盒目录,我们对沙盒目录的文件有权限进行任何操作,也可以用DocumentFile.fromSingleUri来获取一个DocumentFile对象来做操作,但这个对象不是File的子类!
ContentResolver contentResolver = context.getContentResolver();
try {
InputStream is = contentResolver.openInputStream(uri);
File cache = new File(context.getCacheDir().getAbsolutePath(), fileName);
FileOutputStream fos = new FileOutputStream(cache);
FileUtils.copy(is, fos);
file = cache;
fos.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
DocumentFile.fromSingleUri(context, uri).delete();//可以删除文件
}
return file;
}