先看参考:
https://www.jianshu.com/p/959217070c87
https://www.jianshu.com/p/68746e1476a7#comment-22205862
https://www.cnblogs.com/popfisher/archive/2017/08/30/7455754.html
如何查看布局文件
https://blog.csdn.net/nightcurtis/article/details/77734347
工具类,判断服务是否开启,以及跳转到服务页面
import android.content.ContentValues.TAG
import android.content.Context
import android.provider.Settings
import android.text.TextUtils
import android.util.Log
import android.content.Intent
object AssistUtil{
/**
* 检测辅助功能是否开启,第mClas就是下边要写的AccessibilityService 子类
*/
fun isAccessibilitySettingsOn(mContext: Context,mClas :Class<*>): Boolean {
var accessibilityEnabled = 0
val service = mContext.getPackageName() + "/" + mClas.getCanonicalName()
// com.z.buildingaccessibilityservices/android.accessibilityservice.AccessibilityService
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
android.provider.Settings.Secure.ACCESSIBILITY_ENABLED)
Log.v(TAG, "accessibilityEnabled = " + accessibilityEnabled)
} catch (e: Settings.SettingNotFoundException) {
Log.e(TAG, "Error finding setting, default accessibility to not found: " + e.message)
}
val mStringColonSplitter = TextUtils.SimpleStringSplitter(':')
if (accessibilityEnabled == 1) {
Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------")
val settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
// com.z.buildingaccessibilityservices/com.z.buildingaccessibilityservices.TestService
if (settingValue != null) {
mStringColonSplitter.setString(settingValue)
while (mStringColonSplitter.hasNext()) {
val accessibilityService = mStringColonSplitter.next()
Log.v(TAG, "-------------- > accessibilityService :: $accessibilityService $service")
if (accessibilityService.equals(service, ignoreCase = true)) {
Log.v(TAG, "We've found the correct setting - accessibility is switched on!")
return true
}
}
}
} else {
Log.v(TAG, "***ACCESSIBILITY IS DISABLED***")
}
return false
}
fun goSetService(mContext: Context){
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
mContext.startActivity(intent)
}
}
实现步骤
1.实现service
如下,继承AccessibilityService ,
class AssistService : AccessibilityService()
-
清单文件注册
label:我们的系统设置,辅助功能里,有个服务,可以看到我们自定义的这个服务,名字就是label,如下图
image.png
meta-data 里边的resource主要是用来配置这个服务都要监听哪里东西的,也可以在步骤1里的service里配置
<service
android:name=".assitservice.AssistService"
android:exported="true"
android:label="@string/demo_access_server_name1"
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_config" />
</service>
- xml
在res的 xml目录下新建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:canRetrieveWindowContent="true"
android:description="@string/demo_access_server_description1"
android:packageNames="com.xxx.demo0108,com.xxx.wanandroid,com.xxx.demo0327"
android:notificationTimeout="100" />
android:accessibilityEventTypes 就是我们要监听的事件,常用的比如点击事件,通知事件
有很多种,说明可以源码,都有注解的AccessibilityEvent这个类里的
/**
* Mask for {@link AccessibilityEvent} all types.
*
* @see #TYPE_VIEW_CLICKED
* @see #TYPE_VIEW_LONG_CLICKED
* @see #TYPE_VIEW_SELECTED
* @see #TYPE_VIEW_FOCUSED
* @see #TYPE_VIEW_TEXT_CHANGED
* @see #TYPE_WINDOW_STATE_CHANGED
* @see #TYPE_NOTIFICATION_STATE_CHANGED
* @see #TYPE_VIEW_HOVER_ENTER
* @see #TYPE_VIEW_HOVER_EXIT
* @see #TYPE_TOUCH_EXPLORATION_GESTURE_START
* @see #TYPE_TOUCH_EXPLORATION_GESTURE_END
* @see #TYPE_WINDOW_CONTENT_CHANGED
* @see #TYPE_VIEW_SCROLLED
* @see #TYPE_VIEW_TEXT_SELECTION_CHANGED
* @see #TYPE_ANNOUNCEMENT
* @see #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
* @see #TYPE_GESTURE_DETECTION_START
* @see #TYPE_GESTURE_DETECTION_END
* @see #TYPE_TOUCH_INTERACTION_START
* @see #TYPE_TOUCH_INTERACTION_END
* @see #TYPE_WINDOWS_CHANGED
* @see #TYPE_VIEW_CONTEXT_CLICKED
*/
public static final int TYPES_ALL_MASK = 0xFFFFFFFF;
几种常用的
TYPE_WINDOW_STATE_CHANGED
页面状态发生变化,简单理解
对于activity页面onResume就会调用一次, 弹出dialog,popwindow,menu,也都会监听
TYPE_WINDOW_CONTENT_CHANGED
页面有内容发生改变,比如添加或者删除一个view,checkbox选中变成非选中,一个textview的内容变化了等等
TYPE_NOTIFICATION_STATE_CHANGED
这个是监听状态栏来的通知的
//这个是回去notification,notification.contentIntent.send()可以打开通知对应的页面
event.parcelableData is Notification
android:accessibilityFeedbackType
反馈类型,好像这个服务本来是用来给盲人提供帮助的。试了下没啥反应,等测试。。
android:packageNames
这个就是你要监听哪些应用,就把他们的包名写上,多个用逗号隔开即可,没啥说的
android:notificationTimeout
这个可以理解为2次事件的触发间隔时间吧,也不知道对不对。
- 核心的service
下边就是手动设置配置文件,和xml里那个一样的作用
override fun onServiceConnected() {
super.onServiceConnected()
sysout("onServiceConnected=========")
//下边是手动设置监听的信息,也可以xml里配置
// val serverInfo1=AccessibilityServiceInfo();
// serverInfo1.eventTypes=AccessibilityEvent.TYPES_ALL_MASK
// serverInfo1.feedbackType=AccessibilityServiceInfo.FEEDBACK_GENERIC
// serverInfo1.notificationTimeout=100
// serverInfo1.packageNames= arrayOf("com.charlie.demo0108","com.charliesong.wanandroid")
// serviceInfo=serverInfo1
}
然后就是处理系统返回给我们的信息了
override fun onAccessibilityEvent(event: AccessibilityEvent)
//以下是打印的event的信息
event=EventType: TYPE_VIEW_CLICKED; EventTime: 196577485;
PackageName: com.charliesong.demo0327; MovementGranularity: 0; Action: 0
[ ClassName: android.widget.Button; Text: [Kill]; ContentDescription: null; ItemCount: -1;
CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false;
IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1;
ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1;
AddedCount: -1; RemovedCount: -1; ParcelableData: null ];
recordCount: 0
//这个是info的内容,也就是上边的event.resource
info===android.view.accessibility.AccessibilityNodeInfo@80014436;
boundsInParent: Rect(0, 0 - 88, 48); boundsInScreen: Rect(340, 88 - 428, 136);
packageName: com.charliesong.demo0327;
className: android.widget.Button; text: Kill; error: null; maxTextLength: -1; contentDescription: null;
viewIdResName: null; checkable: false; checked: false; focusable: true; focused: false; selected: false;
clickable: true; longClickable: false; contextClickable: false; enabled: true; password: false;
scrollable: false;
actions: [AccessibilityAction: ACTION_FOCUS - null, AccessibilityAction: ACTION_SELECT - null,
AccessibilityAction: ACTION_CLEAR_SELECTION - null, AccessibilityAction: ACTION_CLICK - null,
AccessibilityAction: ACTION_ACCESSIBILITY_FOCUS - null,
AccessibilityAction: ACTION_NEXT_AT_MOVEMENT_GRANULARITY - null,
AccessibilityAction: ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY - null,
AccessibilityAction: ACTION_SET_SELECTION - null, AccessibilityAction: ACTION_UNKNOWN - null]
下边说下常用的操作,肯定是监听到我们要的页面,然后模拟点击操作之类的,
既然要模拟点击操作,肯定要先找到 要点击的控件了,有两种方法
注意点:下边的info可能找不到,比如我们点击一个按钮A,然后这里的inf就是A的信息,你用find方法,就是在这个A里找,肯定找不到的。
这时候要从全局找,如下的方法
rootInActiveWindow.findAccessibilityNodeInfosByViewId
或者使用info.parent 然后在find
var info = event.source//节点node的信息
if (info != null) {
var node:List<AccessibilityNodeInfo>
//如果是带文字的控件,比如textview,button等,可以如下
node=info.findAccessibilityNodeInfosByText("temp")//返回的是一个集合。
//如果不带文字的 ,比如LinearLayout?那么有id也可以的,参数格式, 包名+冒号+id+/+控件的id
findAccessibilityNodeInfosByViewId("${info.packageName}:id/btn_kill")
}
找到我们要操作的控件,执行模拟操作就简单了,如下,ACTION还有其他的,根据实际需要改即可
if(node!=null&&node.size>0){
node[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
补充点知识
1.info.parent 这个返回的也是AccessibilityNodeInfo
和我们平时view的getParent不是一个意思。
这个info.parent包含的所有子child的,它的childcount,是所有基本控件的info
举个例子,如下button3这个info的parent,它的child有5个,就是button1到4以及那个textview1
<LinearLayout>
<LinearLatyou>
<Button1>
<Button2>
</LinearLayout>
<TextView1>
<LinearLatyou>
<Button3>
<Button4>
</LinearLayout>
<LinearLayout>
- Action
ACTION_SET_SELECTION
可以给EditTextView用,让他选中几个文字
val bundle=Bundle().apply {
putInt(ACTION_ARGUMENT_SELECTION_START_INT,3)
putInt(ACTION_ARGUMENT_SELECTION_END_INT,6)
}
val findAccessibilityNodeInfosByViewId("$packageName:id/et_test")?.get(0)?.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION,bundle)
AccessibilityNodeInfo.ACTION_SELECT
这个用listview就好理解了,就是listview的单选,多选模式的选中某个item。
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
对于可滚动的,比如listview,recyclerView,可以往前往后滚动,根据可滚动的方向,测试结果,是把当前item都滚出屏幕,换句话说,滚动的距离就是listview或者recyclerView的高度或者宽度。
ACTION_SET_TEXT
修改view的文本内容,如果是edittextview,很简单的
val arguments=Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,"新的文字")
var result=this.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,arguments)
对于非EditTextView的控件,如果要修改文本,咋办?
测试了下api23的,无能为力
因为这个Action的处理,在api23上,只有Edittextview单独处理,而textview,view都没处理这个action
如下是23的Edittextview的源码
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_SET_TEXT: {
CharSequence text = (arguments != null) ? arguments.getCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;
setText(text);
if (text != null && text.length() > 0) {
setSelection(text.length());
}
return true;
}
default: {
return super.performAccessibilityActionInternal(action, arguments);
}
}
}
然后我试了下api27,看了下源码,action的处理放到了textview下边了,24开始好像就放到这里了。
可以看到,需要enable,并且buffertype为editable即可,正常不做处理基本view都是enable的,所以关键就是buffertype了
case AccessibilityNodeInfo.ACTION_SET_TEXT: {
if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) {
return false;
}
CharSequence text = (arguments != null) ? arguments.getCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;
setText(text);
if (mText != null) {
int updatedTextLength = mText.length();
if (updatedTextLength > 0) {
Selection.setSelection((Spannable) mText, updatedTextLength);
}
}
} return true;
修改buffertyep也简单,给对应的view添加如下两条中的一条即可
android:bufferType="editable"
android:editable="true"
ACTION_DISMISS
看下使用的地方ExpandableNotificationRow,系统类,不可用
case AccessibilityNodeInfo.ACTION_DISMISS:
NotificationStackScrollLayout.performDismiss(this, mGroupManager,
true /* fromAccessibility */);
return true;
错误记录:
尝试修改文字出错
代码以及错误提示如下,我是监听点击事件的,我点击的是个togglebutton,info.text 这行挂了。
然后看下方法的注释里有写 Cannot be called from an AccessibilityService
override fun onAccessibilityEvent(event: AccessibilityEvent) {
var info=event.source
if(info.isChecked){
info.text="aaaaaaaaaaaaa"
}else{
info.text="bbbbbbbbb"
}
java.lang.IllegalStateException: Cannot perform this action on a sealed instance.
点击一个按钮接收到的事件
EventType: TYPE_VIEW_CLICKED;
EventTime: 31572617;
PackageName: com.charlie.demo0108;
MovementGranularity: 0;
Action: 0
[ ClassName: android.widget.Button;
Text: [all];
ContentDescription: null;
ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ];
recordCount: 0
我点击了那个叫 all的按钮,然后想象中,它的parent的child应该就是那4个按钮啊,结果打印结果出乎意料
先看下我的布局
![](https://img.haomeiwen.com/i4625080/1124b571a9322a4c.png)
代码如下
if(TextUtils.equals(Button::class.java.name,info.className)){
if(TextUtils.equals("all",info.text)){
val count=info.parent.childCount;
for( i in 0..count-1){
var childInfo=info.parent.getChild(i);
println("$i=========${childInfo.className}")
}
info.parent.getChild(4).performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
日志在这里
![](https://img.haomeiwen.com/i4625080/1fbc86c9786453c6.png)
实际中测试
1. 一个页面有个recyclerView
现在执行如下操作,点击一个按钮
添加一个view
val textview=TextView(this)
(window.decorView as ViewGroup).addView(textview,layoutParams)
然后打印,可以看到监听TYPE_WINDOW_CONTENT_CHANGED ,event的classname就是
ClassName: android.widget.TextView
一次添加2个view
返回的就是容器了,是个FrameLayout
修改recyclerView的data数据,insert一个数据,notifyItemInserted
监听到的event 的className是recyclerView
同时进行这两种操作,也就是addview和insert item一起进行,结果是啥?
首先TYPE_WINDOW_CONTENT_CHANGED 这个监听到3次
前两个一样的,className是ClassName: android.widget.FrameLayout; Text: []
还有一个是 ClassName: android.support.v7.widget.RecyclerView; Text: []
然后打印了下,发现那2个一样的,event.source?.childCount 其中有一个childcount是2,可getchild 返回的都是null,这个应该是无效的。
所以应该注意了,childcount大于0,完事你getchild不一定存在的
网友评论