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設備創建鏈接了。

相關文章
相關標籤/搜索