Android USB串口通讯实现 以及绕过USB弹框验证

以前公司作了一个新项目,须要将身份证读卡器读取到的照片,姓名,地址信息传输到安卓开发板上,开发板执行人脸对比算法,经过自带的相机和身份证照片对比。java

读卡器和开发板数据传输经过串口通讯实现,这里须要注意的一个地方是,网上搜索Android串口通讯,几乎都是使用jni的方式,由于Android SDK并无在Framework层实现封装关于串口通讯的类库,Android是基于Linux内核,因此咱们能够像在Linux系统上同样来使用串口。这里能够参照Google已经给出了源码,地址在GitHub android-serialport-api 。嗯。。。12年的运行在eclipse里的代码。android

而后再看看咱们的硬件设备:git

  1. Android开发板,型号为RK3288
  2. 身份证读卡器
  3. PL2303HX 芯片的USB转接线

这里有一个坑,公司没人搞过Android串口开发,而网上搜到的都是使用上面的方式进行通讯的。咱们的身份证读卡器的接口是TTL RS232模块,和Android开发板链接须要一个USB转接线,就是下面这个玩意儿:github

经过USB转换了,因此若是使用上面jni的方式,你给读卡器发送命令发送到死,它都不会回应你的(╯°Д°)╯︵┻━┻。因此这里已是USB设备之间进行通讯了。算法

Android系统已经提供了android.hardware.usb.host用于USB设备通讯。那怎么用的呢?问得好!我也不知道。 shell

配置清单文件

<uses-feature android:name="android.hardware.usb.host" android:required="true" />
复制代码

required为true的意思是若是用户设备中没有android.hardware.usb.host这个类库,则没法安装该程序。api

扫描获取设备列表

枚举当前的全部设备,经过vid和pid判断扫描出来的设备是不是本身所须要的设备,若是是本身须要的设备(UsbDevice),则申请使用权限:app

UsbDevice device = null;
private void findSerialPortDevice(){
    UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
    HashMap<String, UsbDevice> usbDevices = usbManager.getDeviceList();
    if (!usbDevices.isEmpty()) {
        boolean keep = true;
        for (Map.Entry<String, UsbDevice> entry : usbDevices.entrySet()) {
            device = entry.getValue();
            int deviceVID = device.getVendorId();
            int devicePID = device.getProductId();
            if (deviceVID != 0x1d6b && (devicePID != 0x0001 && devicePID != 0x0002 && devicePID != 0x0003)) {
                // There is a device connected to our Android device. Try to open it as a Serial Port.
                requestUserPermission();
                keep = false;
            } else {
                device = null;
            }

            if (!keep)
                break;
        }
    }
}

复制代码

上面的代码运行以后,若是没有问题则会获得一个UsbDevice,先看看google文档给出的这个类的解释:eclipse

This class represents a USB device attached to the android device with the android device acting as the USB host. Each device contains one or more UsbInterfaces, each of which contains a number of UsbEndpoints (the channels via which data is transmitted over USB).异步

此类表示链接到Android设备的USB设备,其中android设备充当USB主机。 每一个设备都包含一个或多个UsbInterfaces,每一个UsbInterfaces包含许多UsbEndpoints(至关于一个通道,经过USB来进行数据传输的通道)。

其实这个类就是用来描述USB设备的信息的,能够经过这个类获取到设备的输出输入端口,以及设备标识等信息。

获取到须要的设备以后,请求使用权限:

private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
public static final String ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED";
public static final String ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED";
private void requestUserPermission() {
       Intent intent = new Intent(ACTION_USB_PERMISSION);
       PendingIntent mPermissionIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
       IntentFilter permissionFilter = new IntentFilter(ACTION_USB_PERMISSION);
       context.registerReceiver(usbPermissionReceiver, permissionFilter);
       //申请权限 会弹框提示用户受权
       usbManager.requestPermission(usbDevice, mPermissionIntent);
}
复制代码

这里咱们声明一个广播Receiver,当接受到受权成功的广播后作一些其余处理:

private boolean serialPortConnected;
private UsbDeviceConnection connection;
private final BroadcastReceiver cardReaderReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context arg0, Intent arg1) {
            if (arg1.getAction().equals(ACTION_USB_PERMISSION)) {
                boolean granted = arg1.getExtras().getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED);  //用户是否赞成受权使用usb
                if (granted)
                {
                    connection = usbManager.openDevice(device); //创建一个链接,经过这个链接读写数据
                    new ConnectionThread().start();  //开始读写数据
                }
            } else if (arg1.getAction().equals(ACTION_USB_ATTACHED)) {
                if (!serialPortConnected)
                    findSerialPortDevice();
            } else if (arg1.getAction().equals(ACTION_USB_DETACHED)) {
                serialPortConnected = false;
            }
        }
    };
复制代码

收发数据

受权成功以后,就能够创建一个链接来读写数据了。UsbDeviceConnection就是这个链接

google文档给出的解释是:

This class is used for sending and receiving data and control messages to a USB device. Instances of this class are created by openDevice(UsbDevice).

这个类用于向USB设备发送和接收数据,以及控制消息。 它的实例由openDevice(UsbDevice)这个方法建立。

在这个时候,咱们已经能够和设备进行数据传输了(理论上)。在大部分状况下还须要对USB串口进行一些配置,好比波特率,中止位,数据控制等,否则两边配置不一样,收到的数据会乱码。具体怎么配置,须要看串口设备的芯片是什么了,如今主流的基本上就是PL2303,我使用的转接线也是PL2303的。幸运的是github上有个专门的库UsbSerial,将这些繁琐的配置都打包好了,咱们直接用就行了,使用方法能够去github上看,写得很详细。

发送命令

那怎么给usb外设发送数据呢?UsbDeviceConnection有一个方法用于发送数据:

int bulkTransfer(outEndpoint, data, data.length, TIMEOUT);
复制代码

第一个参数是数据传输的端口,这个端口可不是随便设置的,咱们要找到具备数据传输功能的接口UsbInterface,从它里面找到数据输入和输出端口UsbEndpoint 。

mInterface = device.getInterface(0);  //通常第一个就是咱们须要的
int numberEndpoints = mInterface.getEndpointCount();
for(int i=0;i<=numberEndpoints-1;i++)
{
    UsbEndpoint endpoint = mInterface.getEndpoint(i);
    if(endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK
       && endpoint.getDirection() == UsbConstants.USB_DIR_IN)
        inEndpoint = endpoint;
    else if(endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK
            && endpoint.getDirection() == UsbConstants.USB_DIR_OUT)
        outEndpoint = endpoint;
}
复制代码

第二个参数是发送的数据

第三个参数是数据的大小,最后一个参数是设置超时时间。这个方法的返回值是int类型,它表示本次发送数据成功的字节数,若是失败的话,就返回-1。

接收数据

咱们已经找到了数据输入端口usbEndpointIn,由于数据的输入是不定时的,所以咱们能够另开一个线程,来专门接受数据。

int maxSize = inEndpoint.getMaxPacketSize(); 
ByteBuffer byteBuffer = ByteBuffer.allocate(maxSize); //建立一个缓冲区接收数据
UsbRequest usbRequest = new UsbRequest(); //注意UsbRequest是异步处理的
usbRequest.initialize(connection, inEndpoint); 
usbRequest.queue(byteBuffer, maxSize); 
if(connection.requestWait() == usbRequest){ 
    byte[] retData = byteBuffer.array(); 
    for(Byte byte1 : retData){ 
        Log.d(TAG,byte1)
    } 
}
复制代码

绕过USB系统受权

不知道是Android的bug仍是什么,给usb受权的时候会有一个弹框提醒,虽然能够勾选再也不提示,可是没有任何用,关机重启以后,仍是会从新弹出来。由于是Android开发板,就算外接显示屏,也不会触屏呀!一两台设备还好,外接鼠标搞定,要是上百上千台,那不得累死!

因此有没有什么方法,能够跳过USB受权验证呢?答案是有的。

咱们不须要这个弹框,能够看看点击弹框确认按钮以后作了什么操做。咱们能够模仿点击确认以后的流程,骗过系统。

当弹框出现的时候,能够经过adb shell查看当前的activity:

adb shell dumpsys activity | grep -i run
复制代码

能够清楚的看到当前的activity是UsbPermissionActivity,AndroidSdk里面是能够搜获得这个activity的,个人开发板是6.0的,因此选的android-23,那咱们分析一下这个activity作了些什么。

先把代码所有贴出来:

public class UsbPermissionActivity extends AlertActivity implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener {

    private static final String TAG = "UsbPermissionActivity";

    private CheckBox mAlwaysUse;
    private TextView mClearDefaultHint;
    private UsbDevice mDevice;
    private UsbAccessory mAccessory;
    private PendingIntent mPendingIntent;
    private String mPackageName;
    private int mUid;
    private boolean mPermissionGranted;
    private UsbDisconnectedReceiver mDisconnectedReceiver;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

       Intent intent = getIntent();
        mDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
        mAccessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        mPendingIntent = (PendingIntent)intent.getParcelableExtra(Intent.EXTRA_INTENT);
        mUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
        mPackageName = intent.getStringExtra("package");

        PackageManager packageManager = getPackageManager();
        ApplicationInfo aInfo;
        try {
            aInfo = packageManager.getApplicationInfo(mPackageName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "unable to look up package name", e);
            finish();
            return;
        }
        String appName = aInfo.loadLabel(packageManager).toString();

        final AlertController.AlertParams ap = mAlertParams;
        ap.mIcon = aInfo.loadIcon(packageManager);
        ap.mTitle = appName;
        if (mDevice == null) {
            ap.mMessage = getString(R.string.usb_accessory_permission_prompt, appName);
            mDisconnectedReceiver = new UsbDisconnectedReceiver(this, mAccessory);
        } else {
            ap.mMessage = getString(R.string.usb_device_permission_prompt, appName);
            mDisconnectedReceiver = new UsbDisconnectedReceiver(this, mDevice);
        }
        ap.mPositiveButtonText = getString(android.R.string.ok);
        ap.mNegativeButtonText = getString(android.R.string.cancel);
        ap.mPositiveButtonListener = this;
        ap.mNegativeButtonListener = this;

        // add "always use" checkbox
        LayoutInflater inflater = (LayoutInflater)getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
        ap.mView = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
        mAlwaysUse = (CheckBox)ap.mView.findViewById(com.android.internal.R.id.alwaysUse);
        if (mDevice == null) {
            mAlwaysUse.setText(R.string.always_use_accessory);
        } else {
            mAlwaysUse.setText(R.string.always_use_device);
        }
        mAlwaysUse.setOnCheckedChangeListener(this);
        mClearDefaultHint = (TextView)ap.mView.findViewById(
                                                    com.android.internal.R.id.clearDefaultHint);
        mClearDefaultHint.setVisibility(View.GONE);

        setupAlert();

    }

    @Override
    public void onDestroy() {
    //根据用户的操做决定是否调用service的方法
        IBinder b = ServiceManager.getService(USB_SERVICE);
        IUsbManager service = IUsbManager.Stub.asInterface(b);
        // send response via pending intent
        Intent intent = new Intent();
        try {
            if (mDevice != null) {
                intent.putExtra(UsbManager.EXTRA_DEVICE, mDevice);
                if (mPermissionGranted) {
                    service.grantDevicePermission(mDevice, mUid);
                    if (mAlwaysUse.isChecked()) {
                        final int userId = UserHandle.getUserId(mUid);
                        service.setDevicePackage(mDevice, mPackageName, userId);
                    }
                }
            }
            if (mAccessory != null) {
                intent.putExtra(UsbManager.EXTRA_ACCESSORY, mAccessory);
                if (mPermissionGranted) {
                    service.grantAccessoryPermission(mAccessory, mUid);
                    if (mAlwaysUse.isChecked()) {
                        final int userId = UserHandle.getUserId(mUid);
                        service.setAccessoryPackage(mAccessory, mPackageName, userId);
                    }
                }
            }
            intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, mPermissionGranted);
            mPendingIntent.send(this, 0, intent);
        } catch (PendingIntent.CanceledException e) {
            Log.w(TAG, "PendingIntent was cancelled");
        } catch (RemoteException e) {
            Log.e(TAG, "IUsbService connection failed", e);
        }

        if (mDisconnectedReceiver != null) {
            unregisterReceiver(mDisconnectedReceiver);
        }
        super.onDestroy();
    }

    public void onClick(DialogInterface dialog, int which) {
    //记录下用户的操做
        if (which == AlertDialog.BUTTON_POSITIVE) {
            mPermissionGranted = true;
        }
        finish();
    }

    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (mClearDefaultHint == null) return;

        if(isChecked) {
            mClearDefaultHint.setVisibility(View.VISIBLE);
        } else {
            mClearDefaultHint.setVisibility(View.GONE);
        }
    }
}
复制代码

能够看到的是继承的是AlertActivity,onCreate方法里主要是初始化布局,还有经过intent获取到device和pendingIntent等,这里盲猜一下,应该是经过//盲猜这个mPendingIntent应该是经过usbManager.requestPermission(device, mPendingIntent);传过来的。

这里咱们主要看onDestroy里面作的操做。其实这里已经很清楚的看出来了,若是用户赞成受权,就会调用IUsbManager的方法进行跨进程通讯,

咱们把代码精简一下:

IBinder b = ServiceManager.getService(USB_SERVICE);
IUsbManager service = IUsbManager.Stub.asInterface(b);
Intent intent = new Intent();
intent.putExtra(UsbManager.EXTRA_DEVICE, mDevice);
service.grantDevicePermission(mDevice, mUid);
/*附加信息,能够不要 intent.putExtra(UsbManager.EXTRA_ACCESSORY, mAccessory); service.grantAccessoryPermission(mAccessory, mUid); intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, mPermissionGranted); */
mPendingIntent.send(this, 0, intent);  
复制代码

也就是说,咱们只要模拟上面的代码,便可假装为咱们已经经过了受权。

可是须要注意的一个地方是ServiceManager类和IUsbManager接口都是对开发者隐藏的,不能直接调用,相信走到这一步怎么解决的思路已经很清晰了,无非就是反射调用、修改framework。

这里参考了stackoverflow上面的回答,我使用的方法是建立具备相同包名和彻底相同名称的类:

在个人项目中添加一个包: android.hardware.usb 并在其中放入一个名为IUsbManager.java的文件,再添加一个包:android.os,放入ServiceManager.java 文件。

个人项目是基于RK3288开发板作的,按照stackoverflow上面的修改并无效果,答主也说了他是Android4.0.3的,在其余系统版本上不必定管用。因此我在RK3288官网下载了系统源码,提取了这两个文件,发现还引用了其余文件,总共添加了以下文件:

android.os包下的:
    IServiceManager.java
    ServiceManager.java
    ServiceManagerNative.java

android.hardware.usb包下的:
	IUsbManager.java
//还有一个aidl文件,在android.os包下:
	IPermissionController.aidl
复制代码

这几个文件内容比较多,须要的同窗能够去github自取:usbserial

添加完成以后,整个工程结构是这样的:

最后,修改原来申请受权的方式:

private void requestUserPermission() {
        Intent intent = new Intent();
        intent.setAction(ACTION_USB_PERMISSION);
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        registerReceiver(mReceiver, filter);
        // Request permission
        for (UsbDevice device : usbManager.getDeviceList().values()) {
            intent.putExtra(UsbManager.EXTRA_DEVICE, device);
            intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
            final PackageManager pm = getPackageManager();
            try {
                ApplicationInfo aInfo = pm.getApplicationInfo(getPackageName(),
                        0);
                try {
                    IBinder b = ServiceManager.getService(USB_SERVICE);
                    IUsbManager service = IUsbManager.Stub.asInterface(b);
                    service.grantDevicePermission(device, aInfo.uid);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
            sendBroadcast(intent);  //假装受权成功代码以后,再发送一条广播
        }
    }
复制代码

经测试确实不须要通过弹框受权,直接和USB设备创建链接了。

相关文章
相关标签/搜索