AccessibilityService设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。
最好的资料:官方文档
生命周期
AccessbilityService的生命周期由系统专门管理,并遵循Service的基本生命周期,它只能由用户自己在设置中手动启动,系统绑定到服务后,会调用它的onServiceConnected()方法。
当用户手动在设置中关闭服务,或者开发者调用disableSelf()方法时,该服务会被关闭销毁。
基本配置
- 继承AccessbilityService
class AliAccessibilityService : AccessibilityService() {
private val TAG = "AliAccessibilityService"
//服务中断时的回调
override fun onInterrupt() {
Log.d(TAG, "onInterrupt")
}
//接收到系统发送AccessibilityEvent时的回调
override fun onAccessibilityEvent(event: AccessibilityEvent) {
Log.d(TAG, "onAccessibilityEvent:event:$event")
}
}
- AccessbilityService本质上还是一个Service,所以要在AndroidManifest中注册该服务
<service
android:name=".AliAccessibilityService"
android:label="支付测试"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"是为了确保只有系统可以绑定该服务。
- 配置你需要监听的事件类型、要监听哪个程序,最小监听间隔等属性。这里有两种方式可以进行配置,一种是在manifest中通过meta-data配置,一种是在代码中通过setServiceInfo(AccessibilityServiceInfo)设置。
方式一:通过meta-data设置
<service
android:name=".AliAccessibilityService"
android:label="支付测试"
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/accessible_service_config_ali" />
</service>
accessible_service_config_ali.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds"
android:canRetrieveWindowContent="true"
android:description="@string/pay_test"
android:notificationTimeout="10"
android:canPerformGestures="true"
android:packageNames="com.eg.android.AlipayGphone" />
配置的具体含义我们下面再讲。
方式二:在代码中通过setServiceInfo设置
override fun onServiceConnected() {
val serviceInfo = AccessibilityServiceInfo().apply {
eventTypes = AccessibilityEvent.TYPES_ALL_MASK
feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
packageNames = arrayOf("com.eg.android.AlipayGphone")//支付宝包名,可以多个
notificationTimeout = 10
}
setServiceInfo(serviceInfo)
}
这里我建议使用meta-data的方式进行配置,因为我实践过程中发现有的属性不能通过代码配置。
- 指引用户去手动打开该服务
首先判断该服务是否为开启状态:
public static boolean isAccessibilitySettingsOn(Context mContext, Class<? extends AccessibilityService> clazz) {
int accessibilityEnabled = 0;
final String service = mContext.getPackageName() + "/" + clazz.getCanonicalName();
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
if (accessibilityEnabled == 1) {
String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (settingValue != null) {
mStringColonSplitter.setString(settingValue);
while (mStringColonSplitter.hasNext()) {
String accessibilityService = mStringColonSplitter.next();
if (accessibilityService.equalsIgnoreCase(service)) {
return true;
}
}
}
}
return false;
}
没有开启的话则跳转到该服务的开启页面,由用户手动开启
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
到这里,我们的准备工作就已经完成了,打开支付宝,我们可以看到这样一条日志:
onAccessibilityEvent:event:EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 639278268; PackageName: com.eg.android.AlipayGphone; MovementGranularity: 0; Action: 0 [ ClassName: com.eg.android.AlipayGphone.AlipayLogin; Text: [支付宝]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
其中包括了这个event的类别,当前页面的包名、类名、一些文本信息等,具体有哪些属性可以查看AccessibilityEvent的toString方法。
检索窗口内容
除了监听界面变化之外,我们还可以对界面中的内容进行一些操作,前提是我们配置了以下属性,允许服务检索窗口内容:
android:canRetrieveWindowContent="true"
我们可以通过getWindows获取屏幕上的可以看见的窗口,它返回一个列表,按降序的方式,排在第一个的就是最顶层的窗口。
当然了,一般情况下我们关心的只是最新的那个活动窗口,称为当前活动窗口。我们可以通过以下方法来检索其中的控件:
val root = rootInActiveWindow //获取当前活动窗口的根节点
val node = root.findAccessibilityNodeInfosByViewId("com.alipay.mobile.payee:id/payee_NextBtn") //通过控件id来获取某个控件
val node = root.findAccessibilityNodeInfosByText("确定") //通过text来获取某个控件
val node = root.findFoucs(int falg) //寻找拥有特殊焦点的控件(FOCUS_INPUT 或 FOCUS_ACCESSIBILITY)
至于viewId的获取,我们可以通过android Device Monitor工具来查看,对于3.0之后的android studio,可以通过命令行工具进入sdk的tools目录,运行下面命令:
monitor
交互
事件交互
拿到AccessibilityNodeInfo对象后,我们可以进行一些列的操作,包括getChild()、getParent()、getBoundsInScreen()、isClickable等等一些列获取属性的操作,当然也可以进行交互性的操作,比如点击(当然前提是这个控件的clickable为true):
node[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
除了操作界面内控件之外,我们还可以通过performGlobalAction(int action)执行一些全局操作,比如点击back键、home键等等。
performGlobalAction(GLOBAL_ACTION_BACK)
performGlobalAction(GLOBAL_ACTION_HOME)
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)
performGlobalAction(GLOBAL_ACTION_RECENTS)
手势交互
除了Action交互之外,我们还可以模拟人的手势进行操作,这是在Android 24中新加的一个api:
dispatchGesture(gesture, callback, handler)
它接收一个GestureDescription(手势描述)、一个GestureResultCallback(结果回调)和一个Handler。简单封装一下大概是这样的:
/**
* 通过AccessibilityService在屏幕上模拟手势
* @param path 手势路径
*/
@RequiresApi(Build.VERSION_CODES.N)
fun AccessibilityService.gestureOnScreen(
path: Path,
startTime:Long = 0,
duration:Long = 100,
callback:AccessibilityService.GestureResultCallback,
handler: Handler? = null
){
val builder = GestureDescription.Builder()
builder.addStroke(GestureDescription.StrokeDescription(path, startTime, duration))
val gesture = builder.build()
dispatchGesture(gesture, callback, handler)
}
/**
* 通过AccessibilityService在屏幕上某个位置单击
*/
@RequiresApi(Build.VERSION_CODES.N)
fun AccessibilityService.clickOnScreen(
x:Float,
y:Float,
callback:AccessibilityService.GestureResultCallback,
handler: Handler? = null
){
val p = Path()
p.moveTo(x,y)
gestureOnScreen(p,callback = callback,handler = handler)
}
到这里,AccessibilityService的基本使用方法已经介绍的差不多了,接下来就是根据自己的项目需求进行组装,八仙过海各显神通了。
附录(重要属性介绍)
xml属性 | 说明 | 类别 |
---|---|---|
accessibilityEventTypes | 指定要监听的事件类型 | typeAllMask:接收所有事件。 -------窗口事件相关(常用)--------- typeWindowStateChanged:监听窗口状态变化,比如打开一个popupWindow,dialog,Activity切换等等。 typeWindowContentChanged:监听窗口内容改变,比如根布局子view的变化。 typeWindowsChanged:监听屏幕上显示的系统窗口中的事件更改。 此事件类型只应由系统分派。 typeNotificationStateChanged:监听通知变化,比如notifacation和toast。 -----------View事件相关-------------- typeViewClicked:监听view点击事件。 typeViewLongClicked:监听view长按事件。 typeViewFocused:监听view焦点事件。 typeViewSelected:监听AdapterView中的上下文选择事件。 typeViewTextChanged:监听EditText的文本改变事件。 typeViewHoverEnter、typeViewHoverExit:监听view的视图悬停进入和退出事件。 typeViewScrolled:监听view滚动,此类事件通常不直接发送。 typeViewTextSelectionChanged:监听EditText选择改变事件。 typeViewAccessibilityFocused:监听view获得可访问性焦点事件。 typeViewAccessibilityFocusCleared:监听view清除可访问性焦点事件。 ------------手势事件相关--------------- typeGestureDetectionStart、typeGestureDetectionEnd:监听手势开始和结束事件。 typeTouchInteractionStart、typeTouchInteractionEnd:监听用户触摸屏幕事件的开始和结束。 typeTouchExplorationGestureStart、typeTouchExplorationGestureEnd:监听触摸探索手势的开始和结束。 |
accessibilityFeedbackType | 指定反馈方式 | feedbackAllMask、feedbackGeneric、feedbackAudible、feedbackSpoken、feedbackHaptic、feedbackVisual |
canRetrieveWindowContent | 是否希望能够检索活动窗口内容。此设置无法在运行时更改。 | true or false |
description | 该服务的简要说明 | @StringRes |
notificationTimeout | 两个相同类型的可访问性事件之间的最短间隔时间(以毫秒为单位) | number |
packageNames | 要监听的应用的包名 | string |
canPerformGestures | 是否可以执行手势(api 24新增) | true or false |