android AccessibilityService无障碍功能开发,实现自动化测试,这里使用抖音为例子,仅供技术研究学习使用。
使用方法
安装好APP后,需要打开无障碍功能,打开后,在次打开抖音APP,随便找一个直播间,上下滑动切换直接后,实现模拟点击屏幕,可以自动完成关注。
代码如下
package com.nyw.testclick;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.GestureDescription;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean enabled = isAccessibilityServiceEnabled(this, MyAccessibilityService.class);
//判断是否安装抖音
boolean exist1 = checkAppInstalled(MainActivity.this, "com.ss.android.ugc.aweme");
//抖音极速版
//boolean exist1 = checkAppInstalled(getContext(), "com.ss.android.article.video");
//抖音火山版
//boolean exist1 = checkAppInstalled(getContext(), "com.ss.android.ugc.live");
if (exist1) {
Intent intent = new Intent();
//抖音 打开个人中心 104248958804 需要去获取抖音的UserId
// intent.setData(Uri.parse("snssdk1128://user/profile/104248958804"));
// 打开首页
intent.setData(Uri.parse("snssdk1128://feed?refer=web&gd_label=1"));
//抖音极速版
//intent.setData(Uri.parse("snssdk1112://user/profile/104248958804"));
//抖音火山版
//intent.setData(Uri.parse("snssdk1112://profile?id=104248958804"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} else {
Toast.makeText(MainActivity.this, "请先安装此应用", Toast.LENGTH_SHORT).show();
}
if (enabled==false){
final String actionOpen=Settings.ACTION_ACCESSIBILITY_SETTINGS;//系统辅助功能服务
Intent intent=new Intent(actionOpen);
startActivity(intent);
}
}
/**
* 获取已启用的辅助功能服务
* @param context
* @param service
* @return
*/
public static boolean isAccessibilityServiceEnabled(Context context, Class<? extends AccessibilityService> service) {
AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
List<AccessibilityServiceInfo> enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
for (AccessibilityServiceInfo enabledService : enabledServices) {
ServiceInfo enabledServiceInfo = enabledService.getResolveInfo().serviceInfo;
if (enabledServiceInfo.packageName.equals(context.getPackageName()) && enabledServiceInfo.name.equals(service.getName()))
return true;
}
return false;
}
/**
* 判断是否安装指定包名应用
* @param context
* @param pName
* @return
*/
private boolean checkAppInstalled(Context context, String pName) {
if (pName == null || pName.isEmpty()) {
return false;
}
final PackageManager packageManager = context.getPackageManager();
List<PackageInfo> info = packageManager.getInstalledPackages(0);
if (info == null || info.isEmpty()) {
return false;
}
for (int i = 0; i < info.size(); i++) {
if (pName.equals(info.get(i).packageName)) {
return true;
}
}
return false;
}
}
自定义一个服务MyAccessibilityService,继承AccessibilityService,实现2个方法,重写一个方法,代码如下
package com.nyw.testclick;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.GestureDescription;
import android.content.Context;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.RequiresApi;
import java.util.List;
/**
* 参考文章https://blog.csdn.net/u012758088/article/details/53899485
* 自定义辅助功能服务类AccessibilityService:实现自动化操作的系统服务
*/
public class MyAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//这个函数可以接收系统发送来的AccessibilityEvent,接收来的AccessibilityEvent是经过过滤的,过滤是在配置工作时设置的
//event.getSource() 这是当前event的节点信息
// AccessibilityService.getRootInActiveWindow(); 获取到当前活跃中本服务的可检索到窗口的根节点
// AccessibilityNodeInfo.recycle()//为避免创建重复的实例通过recycle方法回收掉nodeInfo
//event.TYPE_NOTIFICATION_STATE_CHANGED 基本窗口view的变化都可以使用这个type来监听
//event.TYPE_WINDOW_STATE_CHANGED 打开popupwindow,菜单,对话框时候会触发
//event.TYPE_WINDOW_CONTENT_CHANGED 更加精确的代表了基于当前event.source中的子view的内容变化
//event.TYPE_WINDOWS_CHANGED 窗口的变化
//获取到当前活跃中本服务的可检索到窗口的根节点 两种方式获取的childNode个数不一致
// AccessibilityNodeInfo mNodeInfo1 = getRootInActiveWindow();//获取NodeInfo
// AccessibilityNodeInfo mNodeInfo2= event.getSource();//获取NodeInfo
//查找我们需要做操作的view
// List<AccessibilityNodeInfo> listNodes1 =mNodeInfo1. findAccessibilityNodeInfosByViewId("id");//操作的view,查找我们需要操作的对象方法之一
// List<AccessibilityNodeInfo> listNodes2=mNodeInfo2.findAccessibilityNodeInfosByText("id");//操作的view,查找我们需要操作的对象方法之一
// findFocus(0);//查找拥有特定焦点类型的控件
// getRootInActiveWindow();//如果配置能够获取窗口内容,则会返回当前活动窗口的根结点
// getServiceInfo();//获取当前服务的配置信息
// performGlobalAction(0);//执行全局操作,比如返回,回到主页,打开最近等操作
// event.getClassName();//获取事件源对应类的类型,比如点击事件是有某个Button产生的,那么此时获取的就是Button的完整类名
// event. getText();//获取事件源的文本信息,比如事件是有TextView发出的,此时获取的就是TextView的text属性.如果该事件源是树结构,那么此时获取的是这个树上所有具有text属性的值的集合
// event.isEnabled();//事件源(对应的界面控件)是否处在可用状态
// event.getItemCount();//如果事件源是树结构,将返回该树根节点下子节点的数量
try {
int eventType = event.getEventType();//事件类型
String packageName = event.getPackageName().toString();
String className = event.getClassName().toString();
List<AccessibilityNodeInfo> list5 = null;
List<AccessibilityNodeInfo> list6 = null;
List<AccessibilityNodeInfo> list7 = null;
AccessibilityNodeInfo rootNodeInfo = getRootInActiveWindow();
if ("android.widget.ImageView".equals(event.getClassName().toString())) {
perforGlobalClick("com.ss.android.ugc.aweme:id/gsb");
Log.i("sdfkslfsfks", "用户名:" + getTextById("com.ss.android.ugc.aweme:id/m39"));
Log.i("sdfkslfsfks", "文本信息:" + getTextById(" id:com.ss.android.ugc.aweme:id/ag7"));
}
switch (eventType) {
case AccessibilityEvent.TYPE_VIEW_CLICKED:
// 界面点击
Log.i("sdfkslfsfks", "页面点击了");
Log.d("sdfkslfsfks", event.getClassName() + " " + event.getSource().getViewIdResourceName());
// logViewHierarchy(getRootInActiveWindow(), 0);
// perforGlobalClick("com.ss.android.ugc.aweme:id/efr");
break;
case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
// 界面文字改动
Log.i("sdfkslfsfks", "界面文字改动");
perforGlobalClick("com.ss.android.ugc.aweme:id/gsb");
break;
case AccessibilityEvent.TYPE_VIEW_SCROLLED:
//滚动视图的事件。此事件类型通常不直接发送
Log.e("sdfkslfsfks", "onServiceConnected:" + "实现辅助功能");
Log.d("TAG", "packageName = " + packageName + ", className = " + className);
//滑动就自动点赞及关注
perforGlobalClick("com.ss.android.ugc.aweme:id/gsb");
if (className.equals("com.lynx.tasm.behavior.KeyboardMonitor")) {
Log.e("sdfkslfsfks", "执行了搜索按钮");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
list5 = rootNodeInfo.findAccessibilityNodeInfosByViewId("com.ss.android.ugc.aweme:id/rzy");
}
if (null != list5) {
for (AccessibilityNodeInfo info : list5) {
clickByNode(this, info);
}
}
}
if (className.equals("androidx.recyclerview.widget.RecyclerView")) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
list6 = rootNodeInfo.findAccessibilityNodeInfosByText("用户");
}
if (null != list6) {
for (AccessibilityNodeInfo info : list6) {
Log.e("sdfkslfsfks", info.toString());
clickByNode(this, info.getParent());
}
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
list7 = rootNodeInfo.findAccessibilityNodeInfosByText("关注");
}
if (null != list7) {
for (AccessibilityNodeInfo info : list7) {
Log.e("sdfkslfsfks", info.toString());
}
}
}
//获取根节点
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
//匹配Text获取节点
List<AccessibilityNodeInfo> list1 = rootNode.findAccessibilityNodeInfosByText("match_text");
//匹配id获取节点
List<AccessibilityNodeInfo> list2 = rootNode.findAccessibilityNodeInfosByViewId("match_id");
//获取子节点
AccessibilityNodeInfo infoNode = rootNode.getChild(0);
break;
}
}catch (Exception exception){}
}
@Override
public void onInterrupt() {
//在系统将要关闭这个AccessibilityService会被调用。在这个方法中进行一些释放资源的工作。
// disableSelf();//禁用服务。调用此方法后,服务将被禁用,设置将显示它已关闭。
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
//重载的方法
//系统成功绑定该服务时被触发调用这个方法,在这个方法里你可以做一下初始化工作,例如设备的声音震动管理,也可以调用setServiceInfo()进行配置工作
//配置,可在这里配制,也可以在values中添加配制文件进行配制,2种方法二选一
// AccessibilityServiceInfo accessibilityServiceInfo=new AccessibilityServiceInfo();
// accessibilityServiceInfo.eventTypes=AccessibilityEvent.TYPES_ALL_MASK;
// accessibilityServiceInfo.feedbackType=AccessibilityServiceInfo.FEEDBACK_GENERIC;
// accessibilityServiceInfo.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;//获取View的Id
// accessibilityServiceInfo.notificationTimeout=100;
// accessibilityServiceInfo.packageNames=new String[]{"com.nyw.testclick","com.ss.android.ugc.aweme"};//自动点击的包名
// setServiceInfo(accessibilityServiceInfo);//设置当前服务的配置信息
}
// @Override
// protected boolean onKeyEvent(KeyEvent event) {
// //如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生了系统处理按键事件之前
// return super.onKeyEvent(event);
// }
/**
* 借助服务管理器AccessibilityManager来判断,但是该方法不能检测app本身开启的服务。
* @param name
* @return
*/
private boolean enabled(String name) {
AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
List<AccessibilityServiceInfo> serviceInfos = am
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
List<AccessibilityServiceInfo> installedAccessibilityServiceList = am
.getInstalledAccessibilityServiceList();
for (AccessibilityServiceInfo info : installedAccessibilityServiceList) {
Log.d("MainActivity", "all -->" + info.getId());
if (name.equals(info.getId())) {
return true;
}
}
return false;
}
/**
* 系统属性都在settings中进行设置,比如wifi,蓝牙状态等,而这些信息主要是存储在settings对应的的数据库中(system表和serure表)
* ,同样我们也可以通过直接读取setting设置来判断相关服务是否开启
* @param service
* @return
*/
private boolean checkStealFeature1(String service) {
int ok = 0;
try {
ok = Settings.Secure.getInt(getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
}
TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':');
if (ok == 1) {
String settingValue = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (settingValue != null) {
ms.setString(settingValue);
while (ms.hasNext()) {
String accessibilityService = ms.next();
if (accessibilityService.equalsIgnoreCase(service)) {
return true;
}
}
}
}
return false;
}
/**
* 判断页面是否有此文本的view
*/
private boolean viewByText(String str) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(str);
nodeInfo.recycle();
for (AccessibilityNodeInfo item : list) {
if (str.equals(item.getText().toString())) {
return true;
}
}
}
return false;
}
/**
* 获取指定ID的文本
*/
private String getTextById(String id) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(id);
nodeInfo.recycle();
for (AccessibilityNodeInfo item : list) {
return item.getText() + "";
}
}
return "";
}
/**
* 按钮点击事件(View必须是可点击的clickable=true,在uiautomatorviewer中可查看View的属性)
* 根据ID点击某个视图
*/
public void perforGlobalClick(String id) {
try {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(id);
nodeInfo.recycle();
for (AccessibilityNodeInfo item : list) {
item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}catch (Exception exception){}
}
/**
* 基本的递归函数来记录整个可访问性节点视图层次结构
* @param nodeInfo
* @param depth
*/
public static void logViewHierarchy(AccessibilityNodeInfo nodeInfo, final int depth) {
if (nodeInfo == null) return;
String spacerString = "";
for (int i = 0; i < depth; ++i) {
spacerString += '-';
}
//Log the info you care about here... I choce classname and view resource name, because they are simple, but interesting.
Log.d("sdfkslfsfks", spacerString + nodeInfo.getClassName() + " " + nodeInfo.getViewIdResourceName());
for (int i = 0; i < nodeInfo.getChildCount(); ++i) {
logViewHierarchy(nodeInfo.getChild(i), depth+1);
}
}
/**
* 实现位置坐标点击
* @param service
* @param nodeInfo
* @return
*/
public static boolean clickByNode(AccessibilityService service, AccessibilityNodeInfo nodeInfo) {
if (service == null || nodeInfo == null) {
return false;
}
Rect rect = new Rect();
nodeInfo.getBoundsInScreen(rect);
int x = rect.centerX();
int y = rect.centerY();
Log.e("acc_", "要点击的像素点在手机屏幕位置::" + rect.centerX() + " " + rect.centerY());
Point point = new Point(x, y);
GestureDescription.Builder builder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(point.x, point.y);
builder.addStroke(new GestureDescription.StrokeDescription(path, 0L, 100L));
GestureDescription gesture = builder.build();
boolean isDispatched = service.dispatchGesture(gesture, new AccessibilityService.GestureResultCallback() {
@Override
public void onCompleted(GestureDescription gestureDescription) {
super.onCompleted(gestureDescription);
// LogUtil.d(TAG, "dispatchGesture onCompleted: 完成...");
}
@Override
public void onCancelled(GestureDescription gestureDescription) {
super.onCancelled(gestureDescription);
// LogUtil.d(TAG, "dispatchGesture onCancelled: 取消...");
}
}, null);
return isDispatched;
}
}
AndroidManifest.xml文件配制如下
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.nyw.testclick">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestClick"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 配置<intent-filter>,其name为固定的android.accessibilityservice.AccessibilityService-->
<!-- 声明BIND_ACCESSIBILITY_SERVICE权限,以便系统能够绑定该服务(4.1版本后要求)-->
<service
android:name=".MyAccessibilityService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<!-- 代码中配制有了,这里不需要xml配制的,可二选一-->
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessible_service_config" />
</service>
</application>
</manifest>
在xml中添加一个accessible_service_config文件,代码如下文章来源:https://www.toymoban.com/news/detail-518967.html
<?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:accessibilityFlags="flagDefault|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds|flagRequestTouchExplorationMode"
android:canPerformGestures="true"
android:packageNames="com.ss.android.ugc.aweme"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents="true"
android:canRequestEnhancedWebAccessibility="true"
android:settingsActivity="com.nyw.testclick.MyAccessibilityService"
android:notificationTimeout="2000" />
<!-- android:canPerformGestures="true" 允许进行手势操作-->
<!-- android:canRetrieveWindowContent="true" 允许读取页面上的数据-->
<!-- android:accessibilityFlags="flagReportViewIds"获取view id 上线去掉,找VIEW id使用-->
文章来源地址https://www.toymoban.com/news/detail-518967.html
到了这里,关于android AccessibilityService无障碍功能开发,实现自动化测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!