AccessibilityService辅助服务的学习

先看参考:
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()
  1. 清单文件注册
    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>
  1. 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次事件的触发间隔时间吧,也不知道对不对。

  1. 核心的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>
  1. 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个按钮啊,结果打印结果出乎意料
先看下我的布局


image.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)
                        }
                    }

日志在这里


image.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不一定存在的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,204评论 0 17
  • Day1: 在代码中通过R.string.hello_world可以获得该字符串的引用; 在XML中通过@stri...
    冰凝雪国阅读 1,357评论 0 5
  • 一、上节回顾: (一)、三大表单控件中需要记忆的核心方法: 1、RadioButton: RadioGroup类中...
    白话徐文涛阅读 2,046评论 1 7
  • 直面钢铁,飞速旋转 千万个小拳头紧紧团结 一起撞向冰冷和臭硬 沙轮呼啸,沙粒凝神迸劲 谁说是鸡蛋碰石头的不自量力 ...
    小太阳998阅读 201评论 0 1
  • 女神最好的状态就是: 眼里写满了故事, 脸上却不见风霜, 不羡慕谁,不嘲笑谁,也不依赖谁, 吞下了委屈喂大了格局,...
    91c81a7eb707阅读 155评论 0 0