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); } } public static boolean isAccessibilityServiceEnabled(Context context, Class extends AccessibilityService> service) { AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); List 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; } private boolean checkAppInstalled(Context context, String pName) { if (pName == null || pName.isEmpty()) { return false; } final PackageManager packageManager = context.getPackageManager(); List 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;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 listNodes1 =mNodeInfo1. findAccessibilityNodeInfosByViewId("id");//操作的view,查找我们需要操作的对象方法之一// List 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 list5 = null; List list6 = null; List 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 list1 = rootNode.findAccessibilityNodeInfosByText("match_text"); //匹配id获取节点 List 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);// } private boolean enabled(String name) { AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); List serviceInfos = am .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); List installedAccessibilityServiceList = am .getInstalledAccessibilityServiceList(); for (AccessibilityServiceInfo info : installedAccessibilityServiceList) { Log.d("MainActivity", "all -->" + info.getId()); if (name.equals(info.getId())) { return true; } } return false; } 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; } private boolean viewByText(String str) { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { List list = nodeInfo.findAccessibilityNodeInfosByText(str); nodeInfo.recycle(); for (AccessibilityNodeInfo item : list) { if (str.equals(item.getText().toString())) { return true; } } } return false; } private String getTextById(String id) { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { List list = nodeInfo.findAccessibilityNodeInfosByViewId(id); nodeInfo.recycle(); for (AccessibilityNodeInfo item : list) { return item.getText() + ""; } } return ""; } public void perforGlobalClick(String id) { try { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { List list = nodeInfo.findAccessibilityNodeInfosByViewId(id); nodeInfo.recycle(); for (AccessibilityNodeInfo item : list) { item.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } }catch (Exception exception){} } 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); } } 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中添加一个accessible_service_config文件,代码如下
来源地址:https://blog.csdn.net/u013519290/article/details/129395067