美文网首页
AccessibilityService辅助服务的学习

AccessibilityService辅助服务的学习

作者: 有点健忘 | 来源:发表于2019-01-04 15:08 被阅读45次

先看参考:
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不一定存在的

相关文章

网友评论

      本文标题:AccessibilityService辅助服务的学习

      本文链接:https://www.haomeiwen.com/subject/ojpicftx.html