基于AccessibilityService的Android扫码适配实践
使用无障碍服务
无障碍服务即AccessibilityService,是一种,可提供界面增强功能,来协助残障用户或可能暂时无法与设备进行全面互动的用户完成操作的应用。
Google是这样描述的:无障碍服务是一种应用,可提供界面增强功能,来协助残障用户或可能暂时无法与设备进行全面互动的用户完成操作。
从 Android 1.6(API 级别 4)开始,您就可以构建和部署无障碍服务,并且这些服务在 Android 4.0(API 级别 14)中得到了显著改进。Android 支持库也随着 Android 4.0 的发布得到更新,为这些增强的无障碍功能(自 Android 1.6 起)提供支持。如果开发者的目标是打造广泛兼容的无障碍服务,建议他们使用该支持库,并让开发的应用支持 Android 4.0 中引入的更高级的无障碍功能。class ScannerService : AccessibilityService() { override fun onAccessibilityEvent(p0: AccessibilityEvent) { } override fun onInterrupt() { } override fun onKeyEvent(event: KeyEvent): Boolean { LogUtils.d(event) if (Scanner.dispatchKeyEvent(event)) { return true } return super.onKeyEvent(event) } }
AndroidManifest.xml文件中注册ScannerService
keyservice_config.xml这个文件用来声明无障碍服务,最终最后系统设置页面的【无障碍】设置页面中看到相关的开关。 <?xml version="1.0" encoding="utf-8"?>
KeyEvent处理
这里我通过Scanner工具类来处理捕获的KeyEvent,并且在识别到KeyEvent.ACTION_DOWN按键之后将识别内容抛给注册进来的回调函数。最终由回调函数得到了识别到的扫描内容。
另外这里KeyEvent到扫描内容的转换,使用了TextKeyListener。避免了,手动的字符转换,也尽量避免一些兼容性的问题(例如回车换行的识别等)
参考实现如下(主要代码): object Scanner : IScanner { //扫描结果回调 private val callbacks = Stack() private var isStart = false //整体扫描结果 private var mScanResultList: ArrayList = ArrayList() private var handler: Handler? = null private val tkl: KeyListener = TextKeyListener.getInstance(false, TextKeyListener.Capitalize.NONE) private val et = Editable.Factory.getInstance().newEditable("") private val key = View.OnKeyListener { v, keyCode, event -> var returnResult = false if (event.action == KeyEvent.ACTION_DOWN) { val callback = callbacks.peek() if (mScanResultList.size == 0 && callback != null && !isStart) { isStart = true et.clear() callback.scanStart() } returnResult = tkl.onKeyDown(null, et, keyCode, event) } else if (event.action == KeyEvent.ACTION_UP) { LogUtils.d( "Scanner onKeyUp keyCode=${keyCode} UnicodeChar= ${event.unicodeChar}" ) val isEnter = event.keyCode == KeyEvent.KEYCODE_ENTER if (isEnter) { LogUtils.d("Scanner newapi:$et") val mScanResult = et.toString() et.clear() mScanResultList.add(mScanResult) handler?.let { it.removeCallbacks(resultRunnable) it.postDelayed(resultRunnable, 300) } } else { handler?.let { it.removeCallbacks(resultRunnable) } } returnResult = tkl.onKeyUp(null, et, keyCode, event) } else { returnResult = tkl.onKeyOther( null, et, event ) //NOTE: My devices never used KeyEvent.ACTION_MULTIPLE so I don"t know if it should get fired here or with the key down event. } returnResult } override fun initScanner(mContext: Context) { LogUtils.d("initScanner") if (handler == null) { handler = Handler() } } override fun dispatchKeyEvent(event: KeyEvent): Boolean { return key.onKey(null, event.keyCode, event) } override fun registerScanCallback(callback: IScanCallback) { callbacks.add(callback) } override fun unregisterScanCallback(callback: IScanCallback) { callbacks.remove(callback) } override fun destroyScanner() { handler?.let { it.removeCallbacks(resultRunnable) } mScanResultList?.clear() callbacks.clear() } private val resultRunnable = Runnable { val callback = callbacks.peek() if (callback != null) { val result: List = ArrayList(mScanResultList) LogUtils.d("Scan result=${GsonUtils.toJson(result)}") if (result.isNotEmpty()) { callback.scanSuccess(result[0]) } callback.scanSuccess(result) mScanResultList.clear() isStart = false } } }扫码回调处理
通过监听页面的生命周期,自动的将实现过IScanCallback的页面注册到Scanner工具类中,并且在页面销毁的时候自助注销监听,从而实现了监听扫描结果的自动管理。
参考实现如下(主要代码):class AppLifecycleHandler : Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { val isAccessibilitySettingsOn = isAccessibilitySettingsOn(activity.applicationContext) LogUtils.d("AppLifecycleHandler isAccessibilitySettingsOn=$isAccessibilitySettingsOn") if (!isAccessibilitySettingsOn) { Toast.makeText( activity.applicationContext, "请在系统【设置】中【无障碍】页面开启的demo【识别扫码输入】开关", Toast.LENGTH_LONG ).show() openAccessibilitySetting(activity.applicationContext) } if (activity is IScanCallback) { LogUtils.d("AppLifecycleHandler onActivityCreated 扫描监听注册 $activity") registerScanCallback((activity as IScanCallback)) } } override fun onActivityDestroyed(activity: Activity) { if (activity is IScanCallback) { LogUtils.d("AppLifecycleHandler onActivityDestroyed 注销扫描监听注册 $activity") unregisterScanCallback((activity as IScanCallback)) } } }总结
通过android.accessibilityservice.AccessibilityService的onKeyEvent捕获扫码输入,将KeyEvent传递给Scanner处理,Scanner将KeyEvent传递给android.text.method.TextKeyListener,捕获回车按键(或者其他自定义按键的时候)将识别结果抛到回调中,由扫码回调处理扫码结果。