一、构建无障碍服务

  1. 新建MyAccessibilityService.java类继承AccessibilityService
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    @Override
    public void onInterrupt() {

    }
}
  1. 在/res/xml/目录下新建服务配置文件accessibility_config.xml,在里面添加相关的配置信息
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_description"
    android:packageNames="com.example.android.six"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
/>
  • description 要显示在设备辅助功能设置中的本应用辅助功能相关的说明文字
  • packageNames 要监控的应用包名,多个应用用“,”隔开
  • accessibilityEventTypes 此服务希望接收的事件类型
  • accessibilityFlags 辅助功能附加的标志,多个使用“|”分隔,如flagRequestFilterKeyEvents 能够监听到系统的物理按键
  • accessibilityFeedbackType 反馈类型,有语音、视觉、触觉等
  • notificationTimeout 同一类型的两个辅助功能事件发送到服务的最短间隔(毫秒,两个辅助功能事件之间的最小周期)
  • canRetrieveWindowContent 辅助功能服务是否能够取回活动窗口内容的属性
  • settingsActivity 允许用户修改辅助功能的activity组件名称
  1. 在AndroidManifest.xml中进行声明,并引用accessibility_config.xml配置文件
<service
    android:name=".MyAccessibilityService" 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_config" />
</service>
  1. 在辅助功能设置界面开启自身应用的辅助功能开关
Intent intent =  new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);  
startActivity(intent);

二、辅助功能流程解析

  1. AccessibilityService绑定流程
  2. APP的事件分发流程
  3. AccessibilityService执行操作流程

三、可自动执行的操作类型

  1. 控件操作
    通过AccessibilityServiceManager通信到对应APP的控件中,直接操作控件内部逻辑,如performClick、setText
/**
 * 使用方式
 */
AccessibilityNodeInfo.performAction(int action)
AccessibilityNodeInfo.performAction(int action, Bundle arguments)

/**
 * Action that gives input focus to the node.
 * 获取焦点
 */
public static final int ACTION_FOCUS =  0x00000001;

/**
 * Action that clears input focus of the node.
 * 清除焦点
 */
public static final int ACTION_CLEAR_FOCUS = 0x00000002;

/**
 * Action that selects the node.
 * 选中
 */
public static final int ACTION_SELECT = 0x00000004;

/**
 * Action that deselects the node.
 * 清除选中状态
 */
public static final int ACTION_CLEAR_SELECTION = 0x00000008;

/**
 * Action that clicks on the node info.
 *
 * See {@link AccessibilityAction#ACTION_CLICK}
 * 点击
 */
public static final int ACTION_CLICK = 0x00000010;

/**
 * Action that long clicks on the node.
 * 长按
 */
public static final int ACTION_LONG_CLICK = 0x00000020;

/**
 * Action that gives accessibility focus to the node.
 * 获取无障碍功能焦点
 */
public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000040;

/**
 * Action that clears accessibility focus of the node.
 * 清除无障碍功能焦点
 */
public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080;

/**
 * Action that requests to go to the next entity in this node's text
 * at a given movement granularity. For example, move to the next character,
 * word, etc.
 * <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;

/**
 * Action that requests to go to the previous entity in this node's text
 * at a given movement granularity. For example, move to the next character,
 * word, etc.
 * <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;

/**
 * Action to move to the next HTML element of a given type. For example, move
 * to the BUTTON, INPUT, TABLE, etc.
 * <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>
 *
 * 移动到下一个HTML标签
 */
public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400;

/**
 * Action to move to the previous HTML element of a given type. For example, move
 * to the BUTTON, INPUT, TABLE, etc.
 * <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_PREVIOUS_HTML_ELEMENT, arguments);
 * </code></pre></p>
 * </p>
 *
 * 移动到上一个HTML标签
 */
public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800;

/**
 * Action to scroll the node content forward.
 * 向前滚动控件
 */
public static final int ACTION_SCROLL_FORWARD = 0x00001000;

/**
 * Action to scroll the node content backward.
 * 向后滚动控件
 */
public static final int ACTION_SCROLL_BACKWARD = 0x00002000;

/**
 * Action to copy the current selection to the clipboard.
 * 将当前控件内容复制到剪切板
 */
public static final int ACTION_COPY = 0x00004000;

/**
 * Action to paste the current clipboard content.
 * 从剪切板粘贴内容到当前控件
 */
public static final int ACTION_PASTE = 0x00008000;

/**
 * Action to cut the current selection and place it to the clipboard.
 * 将当前控件内容剪切到剪切板
 */
public static final int ACTION_CUT = 0x00010000;

/**
 * Action to set the selection. Performing this action with no arguments
 * clears the selection.
 * <p>
 * <strong>Arguments:</strong>
 * {@link #ACTION_ARGUMENT_SELECTION_START_INT},
 * {@link #ACTION_ARGUMENT_SELECTION_END_INT}<br>
 * <strong>Example:</strong>
 * <code><pre><p>
 *   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);
 * </code></pre></p>
 * </p>
 *
 * @see #ACTION_ARGUMENT_SELECTION_START_INT
 * @see #ACTION_ARGUMENT_SELECTION_END_INT
 *
 * 文本选中操作,可指定选中的开始位置和结束位置,不指定表示清除当前选中
 */
public static final int ACTION_SET_SELECTION = 0x00020000;

/**
 * Action to expand an expandable node.
 * 展开
 */
public static final int ACTION_EXPAND = 0x00040000;

/**
 * Action to collapse an expandable node.
 * 折叠
 */
public static final int ACTION_COLLAPSE = 0x00080000;

/**
 * Action to dismiss a dismissable node.
 * 隐藏,如通知VIEW
 */
public static final int ACTION_DISMISS = 0x00100000;

/**
 * Action that sets the text of the node. Performing the action without argument, using <code>
 * null</code> or empty {@link CharSequence} will clear the text. This action will also put the
 * cursor at the end of text.
 * <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;
  1. 全局操作
    设备的全局操作
/**
 * 使用方式
 */
AccessibilityService.performGlobalAction(int action)

/**
 * Action to go back.
 * 返回键
 */
public static final int GLOBAL_ACTION_BACK = 1;

/**
 * Action to go home.
 * HOME键
 */
public static final int GLOBAL_ACTION_HOME = 2;

/**
 * Action to toggle showing the overview of recent apps. Will fail on platforms that don't
 * show recent apps.
 * MENU键
 */
public static final int GLOBAL_ACTION_RECENTS = 3;

/**
 * Action to open the notifications.
 * 呼出通知栏
 */
public static final int GLOBAL_ACTION_NOTIFICATIONS = 4;

/**
 * Action to open the quick settings.
 * 呼出控制栏
 */
public static final int GLOBAL_ACTION_QUICK_SETTINGS = 5;

/**
 * Action to open the power long-press dialog.
 * 呼出长按电源菜单
 */
public static final int GLOBAL_ACTION_POWER_DIALOG = 6;

/**
 * Action to toggle docking the current app's window
 * 开启分屏模式
 */
public static final int GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN = 7;

/**
 * Action to lock the screen
 * 锁定屏幕
 */
public static final int GLOBAL_ACTION_LOCK_SCREEN = 8;

/**
 * Action to take a screenshot
 * 截屏
 */
public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
  1. 模拟操作
    通过事件注入方式,模拟人为操作,类似ADB操作设备
/**
 * 模拟手势操作点击事件
 *
 * @param service AccessibilityService
 * @param node    节点信息
 * @return 执行结果
 */
public static boolean performClickAction(AccessibilityService service, AccessibilityNodeInfo node) {
    if (node == null) {
        return false;
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Rect rect = new Rect();
        node.getBoundsInScreen(rect);
        Path path = new Path();
        //指定要点击的位置
        path.moveTo(rect.centerX(), rect.centerY());
        GestureDescription.Builder builder = new GestureDescription.Builder();
        //
        builder.addStroke(new GestureDescription.StrokeDescription(path, 0L, 100L));
        GestureDescription gesture = builder.build();

        final CountDownLatch cdl = new CountDownLatch(1);
        final AtomicBoolean res = new AtomicBoolean();
        boolean ret = service.dispatchGesture(gesture, new AccessibilityService.GestureResultCallback() {
            @Override
            public void onCompleted(GestureDescription gestureDescription) {
                super.onCompleted(gestureDescription);
                Log.i(TAG, "gesture performClickAction end Completed");
                res.set(true);
                cdl.countDown();
            }

            @Override
            public void onCancelled(GestureDescription gestureDescription) {
                super.onCancelled(gestureDescription);
                Log.i(TAG, "gesture performClickAction end Cancelled");
                res.set(false);
                cdl.countDown();
            }
        }, null);
        Log.i(TAG, "gesture performClickAction start :" + rect + " | " + ret);
        if (!ret) {
            return false;
        }
        try {
            cdl.await(1, TimeUnit.SECONDS);
        } catch (InterruptedException ignored) {
        }
        return res.get();
    } else {
        return false;
    }
}


/**
 * 模拟屏幕滑动
 * @param service AccessibilityService
 * @param startX 起始X坐标
 * @param startY 起始Y坐标
 * @param endX 结束X坐标
 * @param endY 结束Y坐标
 * @param slideTime 滑动持续时间
 * @return true:成功,false:失败
 */
public static boolean slideScreenAction(AccessibilityService service, int startX, int startY, int endX, int endY, long slideTime) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Path path = new Path();
        //指定滑动起始位置
        path.moveTo(startX, startY);
        //指定滑动结束位置
        path.lineTo(endX, endY);
        GestureDescription.Builder builder = new GestureDescription.Builder();
        GestureDescription description = builder.addStroke(
                new GestureDescription.StrokeDescription(path, 20L, slideTime)).build();
        final CountDownLatch cdl = new CountDownLatch(1);
        final AtomicBoolean res = new AtomicBoolean();
        AccessibilityService.GestureResultCallback callback = new AccessibilityService.GestureResultCallback() {
            @Override
            public void onCompleted(GestureDescription gestureDescription) {
                super.onCompleted(gestureDescription);
                Log.i(TAG, "gesture slide completed");
                res.set(true);
                cdl.countDown();
            }

            @Override
            public void onCancelled(GestureDescription gestureDescription) {
                super.onCancelled(gestureDescription);
                Log.i(TAG, "gesture slide cancelled");
                res.set(false);
                cdl.countDown();
            }
        };
        boolean ret = service.dispatchGesture(description, callback, null);
        ZLog.i(TAG, "gesture slideScreenAction start: " + ret);
        if (!ret) {
            return false;
        }
        try {
            cdl.await(3, TimeUnit.SECONDS);
        } catch (InterruptedException ignored) {
        }
        return res.get();
    }
    return false;
}