安卓设备通过USB与外设通信有两种形式(无需ROOT):

  1. 与设备之间相互发送命令:用串口通信比较多,建议在github搜索felHR85,使用串口通信的前提是,外设支持串口通信且有串口通信的协议,外设不是单纯的存储设备
  2. 只读取设备的文件,有时需要删除文件:
  1. 方法一:通过USB挂载外设到文件系统,然后获取USB权限,把USB设备映射为存储设备,可以在GitHub上搜索libaums框架或者Usb Mass Storage for Android框架,前者是收藏最多的USB存储框架,后者是比较简单好用的框架。两者的实现原理相同。
  2. 方法二:有的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. 1.github搜索并集成上述两种框架的一种
  2. 在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;
    }