Android使用AccessibilityService實(shí)現(xiàn)USB掃碼槍數(shù)據(jù)抓取

android單屏機(jī),通過(guò)掃碼槍掃描二維碼的場(chǎng)景非常多,掃碼槍的種類也有藍(lán)牙、USB、串口等等


目前USB的掃碼槍主流的就是以下兩種

1、USB HID-KBW:掃碼器會(huì)將掃描出來(lái)的內(nèi)容轉(zhuǎn)化為鍵盤事件,就是Android中KeyEvent里面對(duì)應(yīng)的常量(KeyEvent.KEYCODE_*)。

2、USB 虛擬串口:可使用android-serialport-api 連接到UsbDevice進(jìn)行通信,讀取數(shù)據(jù)。(設(shè)備要支持串口)


支持 Android 熱插拔USB掃描槍會(huì)在有EditText時(shí),掃描槍掃描內(nèi)容自動(dòng)輸入到編輯框了,但是有很多輸入法兼容的問(wèn)題,比如搜狗輸入法識(shí)別到HID設(shè)備時(shí)會(huì)隱藏?zé)o法彈出,如果輸入法切換成中文時(shí)會(huì)輸入中文等等。

通過(guò)串口的方式直接獲取原始數(shù)據(jù),不再跟輸入法產(chǎn)生沖突,可惜設(shè)備是USB HID的,通過(guò)大量的嘗試(包括USB虛擬串口)都不支持(對(duì)串口不了解的同學(xué)可以先看看這篇文章上半年最好的Android串口開發(fā)入門指南 - 簡(jiǎn)書

掃碼槍是基于鍵盤輸入的,嘗試從獲取焦點(diǎn)的Activity中的dispatchKeyEvent(KeyEvent event)進(jìn)行攔截,可惜只能解決掉中文的問(wèn)題,事件還是先走到輸入法才能回到Activity。于是強(qiáng)大的AccessibilityService就上場(chǎng)了,使用AccessibilityService可以優(yōu)先獲取到鍵盤事件。

使用強(qiáng)大的AccessibilityService(Google為了讓Android系統(tǒng)更實(shí)用,為用戶提供了無(wú)障礙輔助服務(wù)),但需要到系統(tǒng)設(shè)置->無(wú)障礙->服務(wù) 開啟當(dāng)前服務(wù)。對(duì)AccessibilityService不了解的同學(xué)看看http://www.lxweimin.com/p/4cd8c109cdfb

廢話不多說(shuō)看實(shí)現(xiàn)步驟

1、先創(chuàng)建掃碼Service直接繼承AccessibilityService就OK

public class ScanService extends AccessibilityService {
    private static OnKeyEvent onKeyEvent;
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

    @Override
    protected boolean onKeyEvent(KeyEvent event) {
        if(onKeyEvent!=null){
          //這里通過(guò)回調(diào)的方式將事件傳出去統(tǒng)一處理
          //返回true事件就會(huì)攔截不會(huì)繼續(xù)傳遞
           return onKeyEvent.onKeyEvent(event);
        }
        return super.onKeyEvent(event);
    }
    /**
     * 設(shè)置監(jiān)聽
     * @param onKeyEvent
     */
    public static void setOnKeyEvent(OnKeyEvent onKeyEvent){
        ScanService.onKeyEvent=onKeyEvent;
    }
    public interface OnKeyEvent{
        boolean onKeyEvent(KeyEvent event);
    }
}

2、創(chuàng)建好自己的ScanService后需要在manifest中進(jìn)行注冊(cè)

 <service
            android:name="包名.service.ScanService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService"/>
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility"
                />
</service>

創(chuàng)建android:resource需要用到的xml ,在res下新建xml文件夾,新建accessibility.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagRequestFilterKeyEvents"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:canRequestFilterKeyEvents="true"
    android:description="@string/accessibility_description"
    android:packageNames="包名" />

android:description指定一個(gè)String作為描述文案

<string name="accessibility_description">這里是描述輔助功能的文案</string>

到此為止AccessibilityService就配置好了,你的應(yīng)用就會(huì)出現(xiàn)在系統(tǒng)設(shè)置->輔助功能列表里,只需要手動(dòng)在設(shè)置中打開輔助功能,掃碼槍的鍵盤事件就會(huì)觸發(fā)ScanService的onKeyEvent

接下來(lái)是對(duì)事件的處理
1、過(guò)濾非掃碼槍的設(shè)備

  /**
     * 檢測(cè)輸入設(shè)備是否是掃碼器
     *
     * @param context
     * @return 是的話返回true,否則返回false
     */
    public boolean isInputFromScanner(Context context, KeyEvent event) {
        if (event.getDevice() == null) {
            return false;
        }
//        event.getDevice().getControllerNumber();
        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) {
            //實(shí)體按鍵,若按鍵為返回、音量加減、返回false
            return false;
        }
        if (event.getDevice().getSources() == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD | InputDevice.SOURCE_CLASS_BUTTON)) {
            //虛擬按鍵返回false
            return false;
        }
        Configuration cfg = context.getResources().getConfiguration();
        return cfg.keyboard != Configuration.KEYBOARD_UNDEFINED;
    }

2、處理事件

Runnable mScanningFishedRunnable = new Runnable() {
            @Override
            public void run() {
                String code = mStringBufferResult.toString();
                //做相應(yīng)處理....
                mStringBufferResult.setLength(0);
            }
        };
/**
     * 掃碼槍事件解析
     *
     * @param event
     */
    public void analysisKeyEvent(KeyEvent event) {

        int keyCode = event.getKeyCode();

        //字母大小寫判斷
        checkLetterStatus(event);

        if (event.getAction() == KeyEvent.ACTION_DOWN) {

            char aChar = getInputCode(event);
//            char aChar = (char) event.getUnicodeChar();

            if (aChar != 0) {
                mStringBufferResult.append(aChar);
            }

            if (keyCode == KeyEvent.KEYCODE_ENTER) {
                //若為回車鍵,直接返回
                mHandler.removeCallbacks(mScanningFishedRunnable);
                mHandler.post(mScanningFishedRunnable);
            } else {
                //延遲post,若500ms內(nèi),有其他事件
                mHandler.removeCallbacks(mScanningFishedRunnable);
                mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
            }

        }
    }

    //檢查shift鍵
    private void checkLetterStatus(KeyEvent event) {
        int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                //按著shift鍵,表示大寫
                mCaps = true;
            } else {
                //松開shift鍵,表示小寫
                mCaps = false;
            }
        }
    }


    //獲取掃描內(nèi)容
    private char getInputCode(KeyEvent event) {

        int keyCode = event.getKeyCode();

        char aChar;

        if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
            //字母
            aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
        } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
            //數(shù)字
            aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
            aChar = 0;
        } else {
            //其他符號(hào)
            aChar = (char) event.getUnicodeChar();
        }

        return aChar;

    }

掃描完成,獲取掃描的數(shù)據(jù)后,自己想怎么處理就怎么處理

最后附上一些工具類
跳轉(zhuǎn)到系統(tǒng)輔助功能頁(yè)

  /**
     * 打開設(shè)置-輔助功能頁(yè)
     * @param context
     */
    public void openAccessibilitySetting(Context context){
        context.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
    }

判斷當(dāng)前應(yīng)用的輔助功能在設(shè)置中是否打開

/**
     *
     * @param context
     * @return true輔助功能開 false輔助功能關(guān)
     */
    public boolean isAccessibilitySettingsOn(Context context) {
        int accessibilityEnabled = 0;
        final String service = context.getPackageName() + "/" + ScanService.class.getCanonicalName();
        try {
            //獲取setting里輔助功能的開啟狀態(tài)
            accessibilityEnabled = Settings.Secure.getInt(
                    context.getApplicationContext().getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
        }
        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
        if (accessibilityEnabled == 1) {
            //獲取輔助功能里所有開啟的服務(wù) 包名列表
            String settingValue = Settings.Secure.getString(
                    context.getApplicationContext().getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                //轉(zhuǎn)換程集合
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();
                    //判斷當(dāng)前包名是否在服務(wù)集合里
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

完成。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評(píng)論 25 708
  • 最近做了一個(gè)關(guān)于Android設(shè)備Usb外接掃碼器的項(xiàng)目,在此記錄下。言歸正傳,首先掃碼器有這兩種模式:USB H...
    葫蘆娃大戰(zhàn)屎殼郎閱讀 5,296評(píng)論 9 12
  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    wgl0419閱讀 6,340評(píng)論 1 9
  • 2018年3月20號(hào)的功課: 圖片發(fā)自簡(jiǎn)書App 本周的主題:1,專注,活在當(dāng)下,人生不在于做多少事,在于做了多少...
    再造堂主馮延紅閱讀 301評(píng)論 0 0
  • 今天想拋開一切,寫些碎碎念。天南地北的聊著,任思緒紛飛…… 1.不能跑步的日子 上上周跑長(zhǎng)距離的時(shí)候,為躲過(guò)迎面來(lái)...
    淺心閱讀 346評(píng)論 2 3