先看參考:
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()
-
清單文件注冊(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>
- 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ì)。
- 核心的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>
- 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é)果出乎意料
先看下我的布局

代碼如下
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)
}
}
日志在這里

實(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不一定存在的
