android AccessibilityService无障碍功能开发,实现自动化测试

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文件,代码如下

<?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使用-->

 

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android 中,可以使用 AccessibilityService 来模拟用户的操作,包括返回操作。 以下是实现模拟返回操作的基本步骤: 1. 在 AndroidManifest.xml 文件中声明一个 AccessibilityService: ```xml <service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> ``` 其中,MyAccessibilityService 是自定义的 AccessibilityServiceandroid:resource="@xml/accessibility_service_config" 指定了服务的配置文件。 2. 创建服务的配置文件 accessibility_service_config.xml: ```xml <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews" android:accessibilityFeedbackType="feedbackGeneric" android:notificationTimeout="100" android:canRetrieveWindowContent="true" android:settingsActivity=".SettingsActivity" /> ``` 其中,android:accessibilityEventTypes 指定服务能够处理的事件类型,android:settingsActivity 指定服务的设置界面。 3. 实现 MyAccessibilityService 类: ```java public class MyAccessibilityService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { // 处理事件 } @Override public void onInterrupt() { // 中断服务 } @Override protected boolean onKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { performGlobalAction(GLOBAL_ACTION_BACK); return true; } return super.onKeyEvent(event); } } ``` 其中,onKeyEvent 方法会在用户按下键盘时被调用,如果按下的是返回键,就调用 performGlobalAction(GLOBAL_ACTION_BACK) 方法模拟返回操作。 4. 在应用中启动服务: ```java Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); ``` 以上就是实现模拟返回操作的基本步骤。需要注意的是,使用 AccessibilityService 来模拟用户操作需要用户授权,因此在启动服务时需要跳转到无障碍设置界面让用户手动开启服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值