安卓中的无障碍服务是个很强大的功能。这个服务本来是为了帮助有行动障碍的人群更加便利的使用手机而设计的。但在开发者们的脑洞大开下,人们经常使用无障碍服务来进行一些”骚操作“。
说到无障碍服务,之前我是不知道安卓无障碍服务能干这么多事的,因为我从开始学安卓开始,接触到的大多数教材里,别人写的博客里,大多数的内容都是四大组件及网络通信这方面的内容。包括工作后,掌握的安卓开发技术更多以后,在相当长的一段时间内我都是不知道安卓还有无障碍服务这么一个东西的,直到后来同事告诉我这么个东西,我才恍然大悟,原来什么自动抢红包,自动刷视频,按键脚本啥的,都是可以通过无障碍服务来实现的。
既然无障碍服务这么酷炫,那么我也来做个自己的无障碍服务demo吧,这里我实现的是可以查看当前所监听应用中的某个控件的信息,包括当前activity名,控件id,包名等。顺便实现按音量键就可刷抖音视频的功能(以后吃饭时看抖音就不用刨屏幕了2333)。
先看看实现的效果:
废话不多说,下面就直接招呼代码。
想要实现无障碍服务,就必须先把无障碍服务所需要的配置先配置好,按我下面的步骤,你就可以配置好你的无障碍服务了。
public class MyAccessbilityService extends AccessibilityService { private static boolean isServiceCreated = false; public MyAccessbilityService() { } @Override public void onCreate() { super.onCreate(); //只在第一次创建时调用 } @Override protected void onServiceConnected() { super.onServiceConnected(); isServiceCreated = true; } @Override protected boolean onKeyEvent(KeyEvent event) { return super.onKeyEvent(event); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { //当服务要被中断时调用.会被调用多次 } @Override public void onDestroy() { super.onDestroy(); isServiceCreated = false; } public static boolean isServiceRunning(){ return isServiceCreated; } }
无障碍服务的运行需要在系统的无障碍服务设置中手动将它打开,当你把开关开了后,它就已经运行了,别再做 startservice 这种操作了。它的停止要么你自己在系统设置中的无障碍服中将它关闭,要么调用这个服务的disableSelf()方法。
<service android:name=".MyAccessbilityService" android:enabled="true" android:exported="true" 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/configurate" /> </service>
其中下面框框里的这几个属性都是固定配置的,在官方文档中就是这样写的,所有直接粘贴过去用就行。 至于android:resource="@xml/configurate" ,这个配置文件在下一步中就讲到了。
注意是在res这一级目录下建立xml文件夹,在里面创建一个configurate.xml文件。 文件名可以随意改,但要和manifest中配置的一致即可。
文件内容为:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" 在安卓设置中的无障碍服务设置中你这个应用所显示的名称 android:description="@string/app_accessibility_description" 当前监听的包名,它可以在运行期间动态修改 android:packageNames="com.zhlw.layouthelper" 可以被onAccessibilityEvent收到的事件 android:accessibilityEventTypes="typeViewClicked|typeViewFocused|typeWindowStateChanged" 反馈类型,一般feedbackAllMask就行了 android:accessibilityFeedbackType="feedbackAllMask" 同一个事件的发送间隔 android:notificationTimeout="100" 可以修改无障碍服务属性的类,即只有此类才能对无障碍服务进行动态修改 android:settingsActivity="com.zhlw.layouthelper.MainActivity" 这个大概就说获取当前视图下节点时,是否忽略一些不重要的节点 android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds|flagIncludeNotImportantViews|flagRequestFilterKeyEvents" 接收按键事件吗 android:canRequestFilterKeyEvents="true" 接收窗口改变相关的事件吗 android:canRetrieveWindowContent="true" 是否允许手势动作 android:canPerformGestures="true"/>
比如在oncreate方法中 if (!MyAccessbilityService.isServiceRunning()) { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); }
到此,你的应用打开后,会判断该无障碍服务是否已启动,没启动会引导你去系统设置中打开该服务。
在MyAccessbilityService的onAccessibilityEvent方法中:
switch (event.getEventType()) { case AccessibilityEvent.TYPE_VIEW_FOCUSED: case AccessibilityEvent.TYPE_VIEW_CLICKED: Log.d(TAG, "onAccessibilityEvent: class name"+event.getClassName()); mainFunction.functionHandleFocusAndClick(event,event.getSource()); break; }
其中functionHandleFocusAndClick方法是在我写的mainFunction这个单例类中。
/** * 处理点击和聚焦事件信息 * @param info 节点信息 */ public void functionHandleFocusAndClick(AccessibilityEvent event,AccessibilityNodeInfo info) { String curId = info.getViewIdResourceName().substring(info.getViewIdResourceName().indexOf("/")); if (suspendWindow != null && isWindowShowing() && suspendWindow.isRvMainShowing()){ if (mId.equals(curId)) { //不更新 if (suspendWindow.isAutoUpdateView()){ //已开启自动更新,故强制更新属性信息 mId = curId; if (nodeInfoList.size() > 0) nodeInfoList.clear(); nodeInfoList.add(mId); nodeInfoList.add(activityName);//获取activity名字的 addNodeInfoList(info); suspendWindow.setDataListOrUpdate(getNodeInfoList()); } } else { Log.d(TAG, "functionHandleFocusAndClick: update view info"); if (nodeInfoList.size() > 0) nodeInfoList.clear(); mId = curId; nodeInfoList.add(mId); nodeInfoList.add(activityName);//获取activity名字的 addNodeInfoList(info); suspendWindow.setDataListOrUpdate(getNodeInfoList()); } } }
里面涉及的代码有点多,具体我会在文末放源代码的连接。
我们只要知道,想获取当前点击的控件的信息的话,首先要通过event.getSource()获取到AccessibilityNodeInfo,然后通过AccessibilityNodeInfo中的相关方法就可以拿到控件的信息了,像下面这样
private void addNodeInfoList(AccessibilityNodeInfo info){ nodeInfoList.add(info.getClassName()); nodeInfoList.add(info.getPackageName()); nodeInfoList.add(info.getText()); nodeInfoList.add(info.isCheckable() ? "true" : "false"); nodeInfoList.add(info.isClickable() ? "true" : "false"); nodeInfoList.add(info.isFocusable() ? "true" : "false"); nodeInfoList.add(info.isEditable() ? "true" : "false"); nodeInfoList.add(info.isScrollable() ? "true" : "false"); nodeInfoList.add(info.isChecked() ? "true" : "false"); nodeInfoList.add(info.isEnabled() ? "true" : "false"); nodeInfoList.add(info.isFocused() ? "true" : "false"); nodeInfoList.add(info.isSelected() ? "true" : "false"); nodeInfoList.add(String.valueOf(info.getWindowId())); }
剩下就是展示数据了,我这里是在悬浮窗中显示。
我们无障碍服务中只能接收当前设置了监听的包中传递的事件,还记得在configurate.xml文件中配置的这个属性吗,android:packageNames=“com.zhlw.layouthelper”。 这里就指明对那个应用做监听,不加该属性的话,就是对系统中所有应用都做监听了,这样可能会导致手机卡顿,因此我们必须限制一下对哪些个应用监听,对,它是支持复数个应用的。
在运行前我们可以在configurate.xml中设置要监听哪些个包名,在运行后我们可以通过以下代码动态修改:
AccessibilityServiceInfo serviceInfo = mAccessbilityService.getServiceInfo(); serviceInfo.packageNames = new String[]{pkgName}; mAccessbilityService.setServiceInfo(serviceInfo);
要使用无障碍服务进行模拟滑动的话,我们需要在configurate.xml中配置android:canPerformGestures="true"这个属性。
比如我要上滑一段距离,实现代码如下
public void swipeUpScreen(){ Path path = new Path(); path.moveTo(100,500); path.lineTo(100,300); final GestureDescription.StrokeDescription strokeDescription = new GestureDescription.StrokeDescription(path, 0, 300); mAccessbilityService.dispatchGesture(new GestureDescription.Builder().addStroke(strokeDescription).build(), new AccessibilityService.GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { super.onCompleted(gestureDescription); } @Override public void onCancelled(GestureDescription gestureDescription) { super.onCancelled(gestureDescription); } }, null);//主线程处理即可 }
这里面是使用path作为参数,把我们滑动的轨迹传到GestureDescription.StrokeDescription中,GestureDescription.StrokeDescription的构造方法new GestureDescription.StrokeDescription(path, 0, 300); 中第一个参数是我们的path(即模拟滑动的路径),第二个参数是滑动开始的时间,第三个参数是滑动动作的持续时长(可以理解为动画的duration)。
然后通过AccessbilityService.dispatchGesture方法将这个模拟滑动事件发送到屏幕去。 这个方法也有三个参数。
第一个参数是new GestureDescription.Builder().addStroke(strokeDescription).build(),它就是创建GestureDescription对象,这个是必须的。
第二个参数是new AccessibilityService.GestureResultCallback(),我这里通过匿名内部类实现了滑动事件的回调,这个参数可以传null。
第三个参数是一个handler,即执行在哪个线程,传null的话就是在主线程中执行这次事件。
无障碍服务的其他方面有兴趣大家可以在网上搜搜别人的博客,大多数实现都有教学。 有查不到的,建议去看autojs的源码,它那里面大多数东西都能实现。
最后附上这个项目的源码:layouthelper