AccessibilityService輔助服務(wù)的學(xué)習(xí)

先看參考:
http://www.itdecent.cn/p/959217070c87
http://www.itdecent.cn/p/68746e1476a7#comment-22205862
https://www.cnblogs.com/popfisher/archive/2017/08/30/7455754.html

如何查看布局文件
https://blog.csdn.net/nightcurtis/article/details/77734347

工具類,判斷服務(wù)是否開啟,以及跳轉(zhuǎn)到服務(wù)頁(yè)面

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{
    /**
     * 檢測(cè)輔助功能是否開啟,第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)
    }
}

實(shí)現(xiàn)步驟

1.實(shí)現(xiàn)service
如下,繼承AccessibilityService ,

class AssistService : AccessibilityService()
  1. 清單文件注冊(cè)
    label:我們的系統(tǒng)設(shè)置,輔助功能里,有個(gè)服務(wù),可以看到我們自定義的這個(gè)服務(wù),名字就是label,如下圖


    image.png

    meta-data 里邊的resource主要是用來配置這個(gè)服務(wù)都要監(jiān)聽哪里東西的,也可以在步驟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 就是我們要監(jiān)聽的事件,常用的比如點(diǎn)擊事件,通知事件

有很多種,說明可以源碼,都有注解的AccessibilityEvent這個(gè)類里的

    /**
     * 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

頁(yè)面狀態(tài)發(fā)生變化,簡(jiǎn)單理解
對(duì)于activity頁(yè)面onResume就會(huì)調(diào)用一次, 彈出dialog,popwindow,menu,也都會(huì)監(jiān)聽

TYPE_WINDOW_CONTENT_CHANGED

頁(yè)面有內(nèi)容發(fā)生改變,比如添加或者刪除一個(gè)view,checkbox選中變成非選中,一個(gè)textview的內(nèi)容變化了等等

TYPE_NOTIFICATION_STATE_CHANGED
這個(gè)是監(jiān)聽狀態(tài)欄來的通知的

//這個(gè)是回去notification,notification.contentIntent.send()可以打開通知對(duì)應(yīng)的頁(yè)面
event.parcelableData is Notification
android:accessibilityFeedbackType

反饋類型,好像這個(gè)服務(wù)本來是用來給盲人提供幫助的。試了下沒啥反應(yīng),等測(cè)試。。

android:packageNames

這個(gè)就是你要監(jiān)聽哪些應(yīng)用,就把他們的包名寫上,多個(gè)用逗號(hào)隔開即可,沒啥說的

android:notificationTimeout

這個(gè)可以理解為2次事件的觸發(fā)間隔時(shí)間吧,也不知道對(duì)不對(duì)。

  1. 核心的service

下邊就是手動(dòng)設(shè)置配置文件,和xml里那個(gè)一樣的作用

    override fun onServiceConnected() {
        super.onServiceConnected()
        sysout("onServiceConnected=========")

        //下邊是手動(dòng)設(shè)置監(jiān)聽的信息,也可以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
    }

然后就是處理系統(tǒng)返回給我們的信息了

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

//這個(gè)是info的內(nèi)容,也就是上邊的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]

下邊說下常用的操作,肯定是監(jiān)聽到我們要的頁(yè)面,然后模擬點(diǎn)擊操作之類的,
既然要模擬點(diǎn)擊操作,肯定要先找到 要點(diǎn)擊的控件了,有兩種方法
注意點(diǎn):下邊的info可能找不到,比如我們點(diǎn)擊一個(gè)按鈕A,然后這里的inf就是A的信息,你用find方法,就是在這個(gè)A里找,肯定找不到的。
這時(shí)候要從全局找,如下的方法
rootInActiveWindow.findAccessibilityNodeInfosByViewId
或者使用info.parent 然后在find

var info = event.source//節(jié)點(diǎn)node的信息
        if (info != null) {
var node:List<AccessibilityNodeInfo>
//如果是帶文字的控件,比如textview,button等,可以如下
node=info.findAccessibilityNodeInfosByText("temp")//返回的是一個(gè)集合。
//如果不帶文字的 ,比如LinearLayout?那么有id也可以的,參數(shù)格式, 包名+冒號(hào)+id+/+控件的id
findAccessibilityNodeInfosByViewId("${info.packageName}:id/btn_kill")

}

找到我們要操作的控件,執(zhí)行模擬操作就簡(jiǎn)單了,如下,ACTION還有其他的,根據(jù)實(shí)際需要改即可

if(node!=null&&node.size>0){
                   node[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
               }

補(bǔ)充點(diǎn)知識(shí)

1.info.parent 這個(gè)返回的也是AccessibilityNodeInfo
和我們平時(shí)view的getParent不是一個(gè)意思。
這個(gè)info.parent包含的所有子child的,它的childcount,是所有基本控件的info
舉個(gè)例子,如下button3這個(gè)info的parent,它的child有5個(gè),就是button1到4以及那個(gè)textview1

<LinearLayout>
      <LinearLatyou>
     <Button1>
      <Button2>
      </LinearLayout>
<TextView1>
    <LinearLatyou>
    <Button3>
    <Button4>
     </LinearLayout>
<LinearLayout>
  1. Action
    ACTION_SET_SELECTION
    可以給EditTextView用,讓他選中幾個(gè)文字
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
這個(gè)用listview就好理解了,就是listview的單選,多選模式的選中某個(gè)item。

AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
對(duì)于可滾動(dòng)的,比如listview,recyclerView,可以往前往后滾動(dòng),根據(jù)可滾動(dòng)的方向,測(cè)試結(jié)果,是把當(dāng)前item都滾出屏幕,換句話說,滾動(dòng)的距離就是listview或者recyclerView的高度或者寬度。

ACTION_SET_TEXT
修改view的文本內(nèi)容,如果是edittextview,很簡(jiǎn)單的

val arguments=Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,"新的文字")
var result=this.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,arguments)

對(duì)于非EditTextView的控件,如果要修改文本,咋辦?
測(cè)試了下api23的,無能為力
因?yàn)檫@個(gè)Action的處理,在api23上,只有Edittextview單獨(dú)處理,而textview,view都沒處理這個(gè)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的,所以關(guān)鍵就是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也簡(jiǎn)單,給對(duì)應(yīng)的view添加如下兩條中的一條即可

android:bufferType="editable"
android:editable="true"

ACTION_DISMISS
看下使用的地方ExpandableNotificationRow,系統(tǒng)類,不可用

            case AccessibilityNodeInfo.ACTION_DISMISS:
                NotificationStackScrollLayout.performDismiss(this, mGroupManager,
                        true /* fromAccessibility */);
                return true;

錯(cuò)誤記錄:

嘗試修改文字出錯(cuò)

代碼以及錯(cuò)誤提示如下,我是監(jiān)聽點(diǎn)擊事件的,我點(diǎn)擊的是個(gè)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.

點(diǎn)擊一個(gè)按鈕接收到的事件

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

我點(diǎn)擊了那個(gè)叫 all的按鈕,然后想象中,它的parent的child應(yīng)該就是那4個(gè)按鈕啊,結(jié)果打印結(jié)果出乎意料
先看下我的布局


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

實(shí)際中測(cè)試

1. 一個(gè)頁(yè)面有個(gè)recyclerView

現(xiàn)在執(zhí)行如下操作,點(diǎn)擊一個(gè)按鈕

添加一個(gè)view
val textview=TextView(this)
(window.decorView as ViewGroup).addView(textview,layoutParams)

然后打印,可以看到監(jiān)聽TYPE_WINDOW_CONTENT_CHANGED ,event的classname就是

ClassName: android.widget.TextView
一次添加2個(gè)view

返回的就是容器了,是個(gè)FrameLayout

修改recyclerView的data數(shù)據(jù),insert一個(gè)數(shù)據(jù),notifyItemInserted

監(jiān)聽到的event 的className是recyclerView

同時(shí)進(jìn)行這兩種操作,也就是addview和insert item一起進(jìn)行,結(jié)果是啥?

首先TYPE_WINDOW_CONTENT_CHANGED 這個(gè)監(jiān)聽到3次

前兩個(gè)一樣的,className是ClassName: android.widget.FrameLayout; Text: []
還有一個(gè)是 ClassName: android.support.v7.widget.RecyclerView; Text: []

然后打印了下,發(fā)現(xiàn)那2個(gè)一樣的,event.source?.childCount 其中有一個(gè)childcount是2,可getchild 返回的都是null,這個(gè)應(yīng)該是無效的。
所以應(yīng)該注意了,childcount大于0,完事你getchild不一定存在的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,362評(píng)論 0 17
  • Day1: 在代碼中通過R.string.hello_world可以獲得該字符串的引用; 在XML中通過@stri...
    冰凝雪國(guó)閱讀 1,646評(píng)論 0 5
  • 一、上節(jié)回顧: (一)、三大表單控件中需要記憶的核心方法: 1、RadioButton: RadioGroup類中...
    白話徐文濤閱讀 2,257評(píng)論 1 7
  • 直面鋼鐵,飛速旋轉(zhuǎn) 千萬個(gè)小拳頭緊緊團(tuán)結(jié) 一起撞向冰冷和臭硬 沙輪呼嘯,沙粒凝神迸勁 誰說是雞蛋碰石頭的不自量力 ...
    小太陽998閱讀 254評(píng)論 0 1
  • 女神最好的狀態(tài)就是: 眼里寫滿了故事, 臉上卻不見風(fēng)霜, 不羨慕誰,不嘲笑誰,也不依賴誰, 吞下了委屈喂大了格局,...
    91c81a7eb707閱讀 179評(píng)論 0 0

友情鏈接更多精彩內(nèi)容