观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

  此篇博客主要记录如何开启无障碍服务与功能使用。google的设计这个功能是用来帮助残障人士使用设备。 也能帮助我们开发者进行各种各样的全局事件监听(按键、触控手势、UI变化)这样可以免于修改framework插入事件监听。当然启动条件比较苛刻,需要用户手动打开,所以在正常的应用上应该用不上此功能。但是系统级别的应用上我们可以通过反射直接开启。 还有一些人还会使用此服务进行自动抢微信红包的无语行为。个人是测试转开发,我体验后无障碍服务更像是自动化uiautomator2测试的里的翻版。

添加无障碍服务

第一步 创建AccessibilityService服务类

import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.content.ServiceConnection
import android.util.Log
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent

class AccessibilityService : AccessibilityService() {

    override fun bindService(service: Intent?, conn: ServiceConnection, flags: Int): Boolean {
        return super.bindService(service, conn, flags)
    }

    override fun onCreate() {
        super.onCreate()
    }

    override fun onDestroy() {
        super.onDestroy()
    }

    /**
     * 无障碍服务的生命周期,表明服务已经连接成功
     */
    override fun onServiceConnected() {
        super.onServiceConnected()
    }

    /**
     *当系统想要中断您的服务正在提供的反馈(通常是为了响应将焦点移到其他
     *控件等用户操作)时,就会调用此方法。此方法可能会在您的服务的整个生命
     *周期内被调用多次。
     */
    override fun onInterrupt() {
    }

    /**
     * 当用户在触摸屏上执行特定手势时由系统调用。注意:为了接收手势,
     * 辅助服务必须通过设置AccessibilityServiceInfo请求设备处于触摸探索模式FLAG_REQUEST_TOUCH_EXPLORATION_MOD
     */
    override fun onGesture(gestureId: Int): Boolean {
        Log.e("zh", "onGesture: ${gestureId}")
        return super.onGesture(gestureId)
    }

    /**
     *当系统检测到与无障碍服务指定的事件过滤参数匹配的 AccessibilityEvent
     *时,就会回调此方法。例如,当用户点击按钮,或者聚焦于某个应用(无障碍
     *服务正在为该应用提供反馈)中的界面控件时。出现这种情况时,系统会调用
     *此方法,并传递关联的 AccessibilityEvent,然后服务会对该类进行解释并
     *使用它来向用户提供反馈。此方法可能会在您的服务的整个生命周期内被调用多次。
     */
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        Log.e("zh", "无障碍服务 onAccessibilityEvent:${event}")
        when(event.eventType){
            AccessibilityEvent.TYPE_ANNOUNCEMENT-> Log.e("zh", "应用程序发布公告的事件")
            AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> Log.e("zh", "View的焦点")
            AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED -> Log.e("zh", "View的焦点清除")
            AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> Log.e("zh", "通知栏状态更新")
            AccessibilityEvent.TYPE_VIEW_HOVER_ENTER -> Log.e("zh", "View的鼠标悬停选中")
            AccessibilityEvent.TYPE_VIEW_HOVER_EXIT -> Log.e("zh", "View的鼠标悬停离开")
            AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START -> Log.e("zh", "开始触摸探索手势的事件")
            AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END -> Log.e("zh", "结束触摸探索手势的事件")
            AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> Log.e("zh", "窗口内容更新")
            AccessibilityEvent.TYPE_VIEW_SCROLLED -> Log.e("zh", "滚动类View")
            AccessibilityEvent.TYPE_VIEW_SELECTED -> Log.e("zh", "表示通常在android.widget.AdapterView的上下文中选择项的事件")
            AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> Log.e("zh", "EditText视图选中内容改变")
            AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> Log.e("zh", "EditText视图内容改变")
            AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> Log.e("zh", "表示以给定的移动粒度遍历视图文本的事件")
            AccessibilityEvent.TYPE_VIEW_CLICKED -> Log.e("zh", "点击事件")
            AccessibilityEvent.TYPE_VIEW_LONG_CLICKED -> Log.e("zh", "长按点击事件")
            AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED -> Log.e("zh", "表示在android.view.View上的上下文单击事件")
            AccessibilityEvent.TYPE_GESTURE_DETECTION_START -> Log.e("zh", "开始手势检测")
            AccessibilityEvent.TYPE_GESTURE_DETECTION_END -> Log.e("zh", "结束手势检测")
            AccessibilityEvent.TYPE_TOUCH_INTERACTION_START -> Log.e("zh", "表示用户开始触摸屏幕的事件")
            AccessibilityEvent.TYPE_TOUCH_INTERACTION_END -> Log.e("zh", "表示用户结束触摸屏幕的事件")
            AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT -> Log.e("zh", "表示助手当前正在读取用户屏幕上下文的事件。")
        }
    }

    /**
     * 按键事件
     */
    override fun onKeyEvent(event: KeyEvent): Boolean {
        Log.e("zh", "onKeyEvent: ${event}")
        return super.onKeyEvent(event)
    }
}

 

第二步 在xml资源目录下添加配置xml

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_name"
    android:packageNames="com.zh.XXX,com.android.systemui"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
    android:notificationTimeout="100"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true" />

android:description 此属性是在无障碍服务启用页面的描述

android:packageNames  此属性代表你需要那些应用支持无障碍服务,如果什么都不填删除此属性则代表你想监听设备的全部应用

android:accessibilityEventTypes 事件类型AccessibilityService服务响应的事件类型,只有声明了的类型,系统才会调用该服务的onAccessibilityEvent,有以下几个事件类型提供选择:

  typeViewClicked  点击事件 | typeViewSelected  View被选择 | typeViewScrolled   滑动事件 | typeWindowContentChanged   窗口内容该表 | typeAllMask   所有事件

android:accessibilityFeedbackType 反馈类型

  feedbackSpoken 语音反馈 | feedbackHaptic 触觉(震动)反馈 | feedbackAudible 音频反馈 | feedbackVisual 视频反馈 | feedbackGeneric 通用反馈  | feedbackAllMask 以上都具有

android:accessibilityFlags 额外声明

  flagDefault 默认

  flagIncludeNotImportantViews 

  flagRequestTouchExplorationMode  允许获得触控信息,另外你还需要将android:canRequestTouchExplorationMode  属性设置为true。 请注意!此属性有一定的危险,添加此属性后有可能导致触控失效(触发条件可能是需要插入鼠标或者其他外置设备)

  flagRequestEnhancedWebAccessibility  允许获取Web地址信息,另外你还需要将 android:canRequestEnhancedWebAccessibility   属性设置为true

  flagReportViewIds 允许获得view id,需要获取viewid的时候需要该参数,开始没声明导致nodeInfo. getViewIdResourceName()返回的为null

  flagRequestFilterKeyEvents  此事件添加后才能在服务的onKeyEvent方法里输出当前按键键值,另外你还需要将 android:canRequestFilterKeyEvents 属性设置为true

  flagRetrieveInteractiveWindows 允许获得windows,使用getWindows时需要该参数,否则会返回空列表

android:canRetrieveWindowContent 设置为“true”表示允许获取屏幕信息,使用getWindows、getRootInActiveWindow等函数时需要为“true”

android:canRequestTouchExplorationMode  设置为“true”表示允许获取触摸信息

android:canRequestEnhancedWebAccessibility  设置为“true”表示允许获取Web地址访问信息

android:canRequestFilterKeyEvents  设置为“true”表示允许获取按键信息

android:canRequestFingerprintGestures  设置为“true”表示允许获取手势信息

android:canControlMagnification 设置为“true”表示允许获取缩放信息

android:notificationTimeout  同一种事件类型触发的最短时间间隔(毫秒)

第三步  在AndroidManifest.xml里注册服务

注意在android:resource 属性里添加了上面的配置xml

    <application>

        <service
            android:name=".ScreenSaverAccessibilityService"
            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/accessibility" />
        </service>
    </application>

第四步 启动服务

如果你不是系统级别应用,你需要手动去设置-无障碍中启动服务,如下图

 

 如果你是系统级别应用,这可以使用下面的工具类,实现自动开启无障碍服务:

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;

import java.util.List;

public class AccessibilityUtil {

    /**
     * 关闭无障碍服务
     * @param context
     */
    public static void autoCloseAccessibilityService(Context context){
        if (isStartAccessibilityServiceEnable(context)) {
            String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            ComponentName selfComponentName = new ComponentName(context.getPackageName(), ScreenSaverAccessibilityService.class.getCanonicalName());
            String flattenToString = selfComponentName.flattenToString();
            enabledServicesSetting=enabledServicesSetting.replace(":"+flattenToString , "");

            Settings.Secure.putString(context.getContentResolver(),Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,enabledServicesSetting);
            //Settings.Secure.putInt(context.getContentResolver(),Settings.Secure.ACCESSIBILITY_ENABLED, 0);
            Log.d("zh", "autoCloseAccessibilityService: SETTING ACCESSIBILITY SUCCESS!");
        }
        return;
    }

    /**
     * 开启无障碍服务
     * @param context
     */
    public static void autoOpenAccessibilityService(Context context){
        if (!isStartAccessibilityServiceEnable(context)) {
            String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            ComponentName selfComponentName = new ComponentName(context.getPackageName(), ScreenSaverAccessibilityService.class.getCanonicalName());
            String flattenToString = selfComponentName.flattenToString();
            if (enabledServicesSetting==null||
                    !enabledServicesSetting.contains(flattenToString)) {
                enabledServicesSetting += ":"+flattenToString;
            }
            Settings.Secure.putString(context.getContentResolver(),Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,enabledServicesSetting);
            Settings.Secure.putInt(context.getContentResolver(),Settings.Secure.ACCESSIBILITY_ENABLED, 1);
            Log.d("zh", "autoOpenAccessibilityService: SETTING ACCESSIBILITY SUCCESS!");
        }
        return;
    }

    /**
     * 判断无障碍服务是否开启
     *
     * @param context
     * @return
     */
    public static boolean isStartAccessibilityServiceEnable(Context context) {
        AccessibilityManager accessibilityManager = (AccessibilityManager)context.getSystemService(Context.ACCESSIBILITY_SERVICE);
        assert accessibilityManager != null;
        List<AccessibilityServiceInfo> accessibilityServices = accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
        for (AccessibilityServiceInfo info : accessibilityServices) {
            if (info.getId().contains(context.getPackageName())) {
                return true;
            }
        }
        return false;
    }
}

第五步  如果无障碍服务无法连接或者创建

这可能是google的一些设计,可能是不允许debug安装或者内置系统应用,直接开启无障碍。 你需要重启一下设备就能恢复正常

 

模拟操作

首先在配置xml里一定要添加,否则会出现调用getRootInActiveWindow()始终返回为null的问题

android:canRetrieveWindowContent="true"

单击操作

 这里举例一个单击功能,其他操作其实都是一样可以举一反三的。如果你写过uiautomator2自动化简直是信手拈来。

以文本内容查找View

    /**
     * 根据文本查找点击设置
     */
    fun byTextClickSettings(){
        val nodeInfoList = rootInActiveWindow.findAccessibilityNodeInfosByText("设置")
        //点击
        if (nodeInfoList.isNotEmpty()){
            nodeInfoList[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
        }
    }

以Id查询View

首先需要知道View的id,路径如下,点击monitor.bat:

 

 

 

代码:

    /**
     * 根据id查找点击设置
     */
    fun byIdClickSettings(){
        val nodeInfoList = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.xxx.xxx:id/settings")
        //点击
        if (nodeInfoList.isNotEmpty()){
            nodeInfoList[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
        }
    }

焦点操作

选中焦点

public static final int ACTION_FOCUS =  0x00000001;

清除焦点

public static final int ACTION_CLEAR_FOCUS = 0x00000002;

选中操作

 选中

public static final int ACTION_SELECT = 0x00000004;

清除选中

public static final int ACTION_CLEAR_SELECTION = 0x00000008;

多选

public static final int ACTION_SET_SELECTION = 0x00020000;
        Bundle arguments = new Bundle();
        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1);
        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2);
        info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments);

滚动操作

    /**
     * 操作向前滚动节点内容。
     */
    public static final int ACTION_SCROLL_FORWARD = 0x00001000;

    /**
     * 操作向后滚动节点内容。
     */
    public static final int ACTION_SCROLL_BACKWARD = 0x00002000;

展开/收起操作

    /**
     * 展开可展开节点的操作。
     */
    public static final int ACTION_EXPAND = 0x00040000;

    /**
     * 折叠可展开节点的操作。
     */
    public static final int ACTION_COLLAPSE = 0x00080000;

撤销操作

    /**
     * 撤销可撤销节点的操作。
     */
    public static final int ACTION_DISMISS = 0x00100000;

进度条操作

    /**
     * Argument for specifying the progress value to set.
     * <p>
     * <strong>Type:</strong> float<br>
     * <strong>Actions:</strong>
     * <ul>
     *     <li>{@link AccessibilityNodeInfo.AccessibilityAction#ACTION_SET_PROGRESS}</li>
     * </ul>
     *
     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_SET_PROGRESS
     */
    public static final String ACTION_ARGUMENT_PROGRESS_VALUE =
            "android.view.accessibility.action.ARGUMENT_PROGRESS_VALUE";

移动View操作

    /**
     * Argument for specifying the x coordinate to which to move a window.
     * <p>
     * <strong>Type:</strong> int<br>
     * <strong>Actions:</strong>
     * <ul>
     *     <li>{@link AccessibilityNodeInfo.AccessibilityAction#ACTION_MOVE_WINDOW}</li>
     * </ul>
     *
     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_MOVE_WINDOW
     */
    public static final String ACTION_ARGUMENT_MOVE_WINDOW_X =
            "ACTION_ARGUMENT_MOVE_WINDOW_X";

    /**
     * Argument for specifying the y coordinate to which to move a window.
     * <p>
     * <strong>Type:</strong> int<br>
     * <strong>Actions:</strong>
     * <ul>
     *     <li>{@link AccessibilityNodeInfo.AccessibilityAction#ACTION_MOVE_WINDOW}</li>
     * </ul>
     *
     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_MOVE_WINDOW
     */
    public static final String ACTION_ARGUMENT_MOVE_WINDOW_Y =
            "ACTION_ARGUMENT_MOVE_WINDOW_Y";

复制黏贴操作

    /**
     * 操作将当前选定内容复制到剪贴板。
     */
    public static final int ACTION_COPY = 0x00004000;

    /**
     * 操作粘贴当前剪贴板内容。
     */
    public static final int ACTION_PASTE = 0x00008000;

    /**
     * 操作以剪切当前选定内容并将其放置到剪贴板。
     */
    public static final int ACTION_CUT = 0x00010000;

文本操作

 添加文本

    /**
     * 添加文本内容,如果传入是是null这可以视为清空文本。并且光标会跳转到末尾
     * <p>
     * <strong>Arguments:</strong>
     * {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}<br>
     * <strong>Example:</strong>
     * <code><pre><p>
     *   Bundle arguments = new Bundle();
     *   arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
     *       "android");
     *   info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
     * </code></pre></p>
     */
    public static final int ACTION_SET_TEXT = 0x00200000;

向前选中文本位置

    /**
        请求以给定的移动粒度转到此节点文本中的前一个实体的操作。例如,移动到下一个字符、单词等。
     * <p>
     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
     * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
     * <strong>Example:</strong> Move to the next character and do not extend selection.
     * <code><pre><p>
     *   Bundle arguments = new Bundle();
     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
     *           AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
     *   arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
     *           false);
     *   info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
     *           arguments);
     * </code></pre></p>
     * </p>
     *
     * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
     * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
     *
     * @see #setMovementGranularities(int)
     * @see #getMovementGranularities()
     *
     * @see #MOVEMENT_GRANULARITY_CHARACTER
     * @see #MOVEMENT_GRANULARITY_WORD
     * @see #MOVEMENT_GRANULARITY_LINE
     * @see #MOVEMENT_GRANULARITY_PARAGRAPH
     * @see #MOVEMENT_GRANULARITY_PAGE
     */
    public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200;

向后选中文本位置

    /**
     * 请求以给定的移动粒度转到此节点文本中的下一个实体的操作。例如,移动到下一个字符、单词等。
     * <p>
     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
     * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
     * <strong>Example:</strong> Move to the previous character and do not extend selection.
     * <code><pre><p>
     *   Bundle arguments = new Bundle();
     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
     *           AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
     *   arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
     *           false);
     *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments);
     * </code></pre></p>
     * </p>
     *
     * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
     * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
     *
     * @see #setMovementGranularities(int)
     * @see #getMovementGranularities()
     *
     * @see #MOVEMENT_GRANULARITY_CHARACTER
     * @see #MOVEMENT_GRANULARITY_WORD
     * @see #MOVEMENT_GRANULARITY_LINE
     * @see #MOVEMENT_GRANULARITY_PARAGRAPH
     * @see #MOVEMENT_GRANULARITY_PAGE
     */
    public static final int ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100;

HTML操作

移动HTML元素

    /**
     * 动作移动到给定类型的下一个HTML元素。例如,移动到按钮,输入,表等。
     * <p>
     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
     * <strong>Example:</strong>
     * <code><pre><p>
     *   Bundle arguments = new Bundle();
     *   arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
     *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments);
     * </code></pre></p>
     * </p>
     */
    public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400;

 

 

End

posted on 2023-01-06 18:50  观心静  阅读(6766)  评论(0编辑  收藏  举报