随笔博文

AccessibilityService详解

2022-11-25 13:41:01 michael007js 2806

1、前言

Android的辅助功能是个比较老的API(since API 4),但是该API真正的开始发展还是在API 16~19(很多常用的方法参数都是在这几个版本逐渐加入以及完善的)版本中。辅助功能我们最常见的是在类似深度休眠,抢红包中使用,模拟点击屏幕view。此次写关于辅助功能的分享是因为在API 24、 API 26中,又增添了几个类以及几个接口。

本文首先介绍关于辅助功能的常见使用方法,然后开始介绍辅助功能新增接口以及类的使用。

2、辅助功能的使用

2.1 辅助功能常规使用方法:

本小结介绍一般情况下辅助功能的使用(基础用法和方法释义较多,如果需要阅览API > 20 新增方法使用请从2.2开始阅读),API level在20及以下的使用方式。不涉及新增的方法以及类的使用介绍。

2.1.1 使用流程:

1、Service: 配置自己的辅助功服务(注册、配置)

2、Class: 自己的辅助功能类(对事件的拦截、处理)

2.1.2 Service配置:

1、在实例化的辅助功能类中:onServiceConnected()中初始化,如下所示。

@Override

protected void onServiceConnected() {
    super.onServiceConnected();
    AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
    info.notificationTimeout = 100;
    info.packageNames = new String[]{"...", "..."};
    info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
    setServiceInfo(info);
}

2、使用manifest中添加meta-data的方式(该方式从API 14开始使用)

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes=""
    android:accessibilityFeedbackType=""
    android:accessibilityFlags=""
    android:canRequestEnhancedWebAccessibility=""
    android:canRequestFilterKeyEvents=""
    android:canRequestTouchExplorationMode=""
    android:canRetrieveWindowContent=""
    android:description=""
    android:notificationTimeout=""
    android:packageNames=""
    android:settingsActivity=""
    android:summary="" />

由上边两组代码可以看到,前一个配置中单个代码块中,配置属性有限,但灵活的在服务里边初始化,使我们可以随时的改变需要的flag和type及一些其他参数。而后一个(Android 4.0+ / API 14+)的meta-data配置方式,可配置的属性更多。在系统条件符合时(同时不需要更改我们辅助功能的一些参数时),还是推荐使用后一个配置方式进行辅助功能的配置。

2.1.3 AccessibilityServiceInfo 配置参数:

这里只是对所有的XML attributes进行说明。

XML attributes:

constant value 描述
typeAllMask ffffffff 所有类型的事件
typeAnnouncement 4000 一个应用产生一个通知事件
typeAssistReadingContext 1000000 辅助用户读取当前屏幕事件
typeContextClicked 800000 view中上下文点击事件
typeGestureDetectionEnd 80000 监测到的手势事件完成
typeGestureDetectionStart 40000 开始手势监测事件
typeNotificationStateChanged 40 收到notification弹出消息事件
typeTouchExplorationGestureEnd 400 触摸浏览事件完成
typeTouchExplorationGestureStart 200 触摸浏览事件开始
typeTouchInteractionEnd 200000 用户触屏事件结束
typeTouchInteractionStart 100000 触摸屏幕事件开始
typeViewAccessibilityFocusCleared 10000 无障碍焦点事件清除
typeViewAccessibilityFocused 8000 获得无障碍的焦点事件
typeViewClicked 1 点击事件
typeViewFocused 8 view获取到焦点事件
typeViewHoverEnter 80 一个view的悬停事件
typeViewHoverExit 100 一个view的悬停事件结束,悬停离开该view
typeViewLongClicked 2 view的长按事件
typeViewScrolled 1000 view的滚动事件,adapterview、scrollview
typeViewSelected 4 view选中,一般是具有选中属性的view,例如adapter
typeViewTextChanged 10 edittext中文字发生改变的事件
typeViewTextSelectionChanged 2000 edittext文字选中发生改变事件
typeViewTextTraversedAtMovementGranularity 20000 UIanimator中在一个视图文本中进行遍历会产生这个事件,多个粒度遍历文本。一般用于语音阅读context
typeWindowContentChanged 800 窗口的内容发生变化,或者更具体的子树根布局变化事件
typeWindowStateChanged 20 新的弹出层导致的窗口变化(dialog、menu、popupwindow)
typeWindowsChanged 400000 屏幕上的窗口变化事件,需要API 21+
constant value 描述
feedbackAllMask ffffffff 取消所有的可用反馈方式
feedbackAudible 4 可听见的(非语音反馈)
feedbackGeneric 10 通用反馈
feedbackHaptic 2 触觉反馈(震动)
feedbackSpoken 1 语音反馈
feedbackVisual 8 视觉反馈
  • accessibilityFlags 辅助功能附加的标志,多个使用 ‘ | ‘分隔
constant value 描述
flagDefault 1 默认的配置
flagEnableAccessibilityVolume 80 这个标志要求系统内所有的音频通道,使用由STREAM_ACCESSIBILTY音量控制USAGE_ASSISTANCE_ACCESSIBILITY
flagIncludeNotImportantViews 2 表示可获取到一些被表示为辅助功能无权获取到的view
flagReportViewIds 10 使用该flag表示可获取到view的ID
flagRequestAccessibilityButton 100 如果辅助功能可用,提供一个辅助功能按钮在系统的导航栏 API 26+
flagRequestEnhancedWebAccessibility 8 此类扩展的目的是为WebView中呈现的内容提供更好的辅助功能支持。这种扩展的一个例子是从一个安全的来源注入JavaScript。如果至少有一个具有此标志的辅助功能服务, 则系统将使能增强的web辅助功能。因此, 清除此标志并不保证该设备不会使能增强的web辅助功能, 因为可能有另一个使能的服务在使用它。
flagRequestFilterKeyEvents 20 能够监听到系统的物理按键
flagRequestFingerprintGestures 200 监听系统的指纹手势 API 26+
flagRequestTouchExplorationMode 4 系统进入触控探索模式。出现一个鼠标在用户的界面
flagRetrieveInteractiveWindows 40 该标志知识的辅助服务要访问所有交互式窗口内容的系统,这个标志没有被设置时,服务不会收到TYPE_WINDOWS_CHANGE事件。
  • canRequestEnhancedWebAccessibility (boolean)
    辅助功能服务是否能够请求WEB辅助增强的属性。例如: 安装脚本以使应用程序内容更易于访问。
  • canRequestFilterKeyEvents (boolean)
    辅助功能服务是否能够请求过滤KeyEvent的属性,是否可以请求KeyEvent事件流。,flagRequestFilterKeyEvents搭配使用
  • canRequestTouchExplorationMode (boolean)
    此属性用于,能够让辅助功能服务通过手势,来请求触摸浏览模式,其被触摸的项,将被朗读出来,flagRequestTouchExplorationMode搭配使用
  • canRetrieveWindowContent (boolean)
    辅助功能服务是否能够取回活动窗口内容的属性。 与上边的flagRetrieveInteractiveWindows搭配使用,无法在运行时更改此设置。
  • description
    辅助功能服务目的或行为的简短描述。
  • notificationTimeout
    同一类型的两个辅助功能事件发送到服务的最短间隔(毫秒,两个辅助功能事件之间的最小周期)
  • packageNames
    从此服务能接收到事件的软件包名称 (不适合所有软件包)(多个软件包用逗号分隔)。
  • settingsActivity
    允许用户修改辅助功能的activity组件名称
  • summary
    同description

2.1.4 实现自己的辅助功能类:

首先需要了解的是AccessibilityService这个类。

Public方法有:

Method 描述
API 20
getRootInActiveWindow() 获取窗体中的节点信息。 返回 AccessibilityNodeInfo
getServiceInfo() 获取本服务的配置信息。 返回 AccessibilityServiceInfo
getSystemService(String name)) 通过服务名获取对应的系统服务。 返回 Object
onAccessibilityEvent(AccessibilityEvent event) 辅助功能事件(s)回调方法
onBind(Intent intent) service的bind接口
onInterrupt() 辅助功能中断的回调
performGlobalAction(int action) 全局的点击方法
setServiceInfo(AccessibilityServiceInfo info) 设置该辅助功能服务的描述,修改辅助功能的配置。可通过搭配使用getServiceInfo()动态修改我们辅助功能的配置
API 21新增:
getWindows() 返回窗口最上层的一个用户可交互的窗口信息节点 List<AccessibilityWindowInfo>
API 24新增:
disableSelf() 关闭自己service的方法,在设置界面可以看到辅助功能状态被关闭
getSoftKeyboardController() 返回软键盘控制器,可用于查询和修改软键盘显示模式。
findFocus(int focus) 找到具有指定焦点类型的视图。搜索在所有窗口中执行。注意:为了访问Windows,您的服务必须通过在其元数据中设置AccessibilityService_canRetrieveWindowContent属性来声明检索窗口内容的功能。有关详细信息,请参阅SERVICE_META_DATA。此外,服务必须选择通过设置FLAG_RETRIEVE_INTERACTIVE_WINDOWS标志来检索交互式窗口。否则,搜索将仅在活动窗口中执行。
getMagnificationController() 返回放大控制器,可用于查询和修改显示放大的状态。注意:为了控制放大倍数,您的服务必须通过在其元数据中设置AccessibilityService_canControlMagnification属性来声明该功能
dispatchGesture(GestureDescription ge, GestureResultCallback cb, Handler handler) 将手势发送到触摸屏。目前正在进行的任何手势(无论是从用户,本服务还是其他服务)将被取消。 手势将被调度,就像在用户直接在屏幕上执行的一样,因此事件可能会受到诸如放大和触摸探索之类的功能的影响。
API 26新增:
getAccessibilityButtonController() 返回系统导航区域中可访问性按钮的控制器。当设置FLAG_REQUEST_ACCESSIBILITY_BUTTON时,此实例可用于查询辅助功能按钮的状态并注册监听器以进行交互和辅助功能按钮的状态更改。返回 AccessibilityButtonController
getFingerprintGestureController() 获取控制器的指纹手势。此功能需要 AccessibilityServiceInfo#CAPABILITY_CAN_CAPTURE_FINGERPRINT_GESTURES。注意:在调用此方法之前,必须先启动该服务。返回 FingerprintGestureController

这部分我们只针对API <= 20部分的代码,其中我们常用的方法有:

Method 描述
getRootInActiveWindow() 获取窗体中的节点信息 AccessibilityNodeInfo,如果需要使用该方法,我们需要在配置中申明 canRetrieveWindowContent,否则可能获取不到窗体的节点信息。AccessibilityNodeInfo
onAccessibilityEvent(AccessibilityEvent event) 辅助功能事件(s)回调方法,处理辅助功能内所有事件的回调方法
performGlobalAction(int action) 全局的点击方法

在使用这些方法时,有三个需要我们了解的地方:

  • AccessibilityEvent 在辅助功能event回调方法中,返回的参数。(官方API En)
  • AccessibilityNodeInfo 辅助功能中的节点对象(nodeInfo)。(官方API En)
  • performGlobalAction(action) 全局事件发送方法(物理按键的点击)

AccessibilityEvent

当用户界面发生某些明显的事件时,AccessibilityEvent代表的无障碍事件会被系统发送,每一种事件类型是由该类暴露出的属性子集表示其特征的。在此类中为每一种事件类型定义了相应的常量。详细的事件类型请参照AccessibilityEvent文档 Zh

Public Methods 公有方法:

Method 描述
appendRecord(AccessibilityRecord record) AccessibilityRecord追加到事件记录的结尾。
getRecord(int index) 获取给定索引下的记录。
getRecordCount() 获取被包含在事件中的记录数。
describeContents() 描述被包含在 Parcelable 实例的编组表示中的特殊对象种类。例如,如果对象在 writeToParcel(Parcel, int)的输出里包括一个文件描述符,该方法的返回值必须包括 CONTENTS_FILE_DESCRIPTOR 位。
getEventType() 获取事件类型。
setEventType(int eventType) 设置事件类型。
eventTypeToString(int eventType) 返回一个事件类型的字符串表示。例如,TYPE_VIEW_CLICKED 的字符串表示为“TYPE_VIEW_CLICKED”。
getAction() 获取触发该事件的执行操作。
getContentChangeTypes() 获取由 TYPE_WINDOW_CONTENT_CHANGED 事件标识的改变类型的位掩码。一个单一事件可能代表多种变化类型。
getEventTime() 获取该事件的发送时间。
setEventTime(long eventTime) 设置事件被发送的时间。
getMovementGranularity() 获取遍历的移动粒度。
setMovementGranularity(int granularity) 设置遍历的移动粒度。
getPackageName() 获取源的包名。
setPackageName(CharSequence packageName) 设置源的包名。
obtain(AccessibilityEvent event) 如果可获得,返回一个缓存实例或创建一个新实例。返回实例从给定事件初始化。
obtain() 如果可获得,返回一个缓存实例或实例化一个新的。
obtain(int eventType) 如果可获得,返回一个缓存实例或实例化一个新的并设置它的类型属性。
recycle() 回收一个实例重复使用。
setAction(int action) 设置触发此事件的执行操作。
setContentChangeTypes(int changeTypes) 设置由一个 TYPE_WINDOW_CONTENT_CHANGED 事件标识的节点树改变的位掩码。
toString() 返回对象的字符串表示。一般情况下,toString 方法返回一个“文本表示”该对象的字符串。结果应该是一个简洁但容易阅读的信息表示。建议所有子类重写该方法。
writeToParcel(Parcel parcel, int flags) 整组该对象到一个 Parcel。

AccessibilityNodeInfo

该类代表一个窗口内容节点和可以从源请求的操作。从 AccessibilityService的角度看,一个窗口内容被呈现为一个无障碍节点信息树,该树可能与视图层次一一映射,也可能不与视图层次一一映射。换句话说,一个自定义视图可灵活地将自己报告为一个无障碍节点信息树。一旦无障碍节点信息被发送给无障碍服务,该信息将会是不可改变的,且调用状态改变方法将会产生错误。AccessibilityNodeInfo文档 Zh

嵌套类:

常量:
使用nodeInfo.performAction(Int Action)performAction(Int Action, Bundle bundle)方法

Action 释义
ACTION_ACCESSIBILITY_FOCUS 给节点添加无障碍焦点的操作。
ACTION_ARGUMENT_COLUMN_INT 让指定集合列在屏幕上可见的参数。
ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN 当以一定粒度移动时,是否扩大选择范围或反之移除的参数。
ACTION_ARGUMENT_HTML_ELEMENT_STRING 要移动到的下一个/上一个 HTML 元素的参数。
ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT 当遍历节点文本的时,使用哪种移动粒度的参数。。
ACTION_ARGUMENT_PROGRESS_VALUE 指定要设置的进度值的参数。
ACTION_ARGUMENT_ROW_INT 让指定集合行在屏幕上可见的参数。
ACTION_ARGUMENT_SELECTION_END_INT 指定选择结束的参数。
ACTION_ARGUMENT_SELECTION_START_INT 指定选择起始的参数。
ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE 指定要设置的文本内容的参数。
ACTION_CLEAR_ACCESSIBILITY_FOCUS 清除节点无障碍焦点的操作。
ACTION_CLEAR_FOCUS 清除节点输入焦点的操作。
ACTION_CLEAR_SELECTION 取消选择节点的操作。
ACTION_NEXT_AT_MOVEMENT_GRANULARITY 以给定移动粒度,请求去到该节点文本的下一个文本实体的操作。
ACTION_CLICK 在节点信息上点击的操作.
ACTION_COLLAPSE 折叠一个可展开节点的操作。
ACTION_COPY 将当前选择拷贝到剪贴板的操作。
ACTION_CUT 剪贴当前选项并放置到剪贴板的操作。
ACTION_DISMISS 关闭一个可关闭节点的操作。
ACTION_EXPAND 展开一个可展开节点的操作。
ACTION_FOCUS 给节点添加输入焦点的操作。
ACTION_LONG_CLICK 在节点上点击长按的操作。
ACTION_NEXT_HTML_ELEMENT 移动到给定类型的下一个 HTML 元素的操作。例如,移动到 BUTTON、INPUT、TABLE 等。
ACTION_PASTE 粘贴当前剪贴板内容的操作。
ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY 以给定移动粒度,请求去到该节点文本的上一个文本实体的操作。例如,移动到下一个字、词等。
ACTION_PREVIOUS_HTML_ELEMENT 移动到给定类型的上一个 HTML 元素的操作。例如,移动到BUTTON、INPUT、TABLE 等。
ACTION_SCROLL_BACKWARD 向后滚动节点内容的操作。
ACTION_SCROLL_FORWARD 向前滚动节点内容的操作
ACTION_SELECT 选择节点的操作。
ACTION_SET_SELECTION 设置选择项的操作。执行该操作,并且无参数清除选项
ACTION_SET_TEXT 设置节点文本的操作。在没有参数的情况下执行该操作,使用 null 或者空CharSequence 将会清除文本。该操作也将会把光标放置到文本末尾
FOCUS_ACCESSIBILITY 无障碍焦点。
FOCUS_INPUT 输入焦点。
MOVEMENT_GRANULARITY_CHARACTER 以字符为移动粒度位,遍历节点文本
MOVEMENT_GRANULARITY_LINE 以行为移动粒度位,遍历节点文本。
MOVEMENT_GRANULARITY_PAGE 以页为移动粒度位,遍历节点文本。
MOVEMENT_GRANULARITY_PARAGRAPH 以段为移动粒度位,遍历节点文本。
MOVEMENT_GRANULARITY_WORD 以字词为移动粒度位,遍历节点文本。

方法:

Public Method 释义
addAction(int action) 为一个nodeInfo添加一个操作,在API 21时被弃用
addAction(AccessibilityNodeInfo.AccessibilityAction action) 为一个nodeInfo添加一个操作,API 21
addChild(View root, int virtualDescendantId) 添加一个虚拟子元素,作为给定根节点的子view
addChild(View child) 添加一个子view节点。
canOpenPopup() 获取该节点是否可以打开一个弹窗或对话框。
describeContents() 描述被包含在可打包实例的封装代表中的特殊对象的种类。
equals(Object object) 标识是否有某些其他对象“等同于”该对象。
findAccessibilityNodeInfosByText(String text) 使用文本找到 AccessibilityNodeInfo。
findAccessibilityNodeInfosByViewId(String viewId) 使用完全合格视图id的源名称找到AccessibilityNodeInfo,完全合格 id 的样式 如下“package:id/id_resource_name”。
findFocus(int focus) 找到具有指定焦点类型的视图。
focusSearch(int direction) 在指定方向搜索具有输入焦点的最近的视图。
getActionList() 获取可以在该节点上执行的操作。
getActions() 该方法在 API 级别 21 被弃用。使用getActionList()代替。
getBoundsInParent(Rect outBounds) 获取父级坐标中的节点边界。
getBoundsInScreen(Rect outBounds) 获取屏幕坐标中的节点边界。
getChild(int index) 获取给定索引下的子元素。
getChildCount() 获取子元素的数目。
getClassName() 获取该节点来自的类。
getCollectionInfo() 如果节点是个集合,获取集合信息。一个集合子集总是一个集合项目。
getCollectionItemInfo() 如果节点是个集合项目,获取集合项目信息。一个集合项目总是一个集合的子集。
getContentDescription() 获取该节点的内容描述。
getExtras() 获取具有附加数据的可选 bundle。该包是闲时创建的且永不为 null。注意:为了避免冲突,推荐使用应用的包名作为关键字字首,如果从不同应用发出的相同关键词有不同的含义,可能会导致无障碍服务困扰。
getInputType() 获取定义为 InputType 的源中的输入类型。
getLabelFor() 为无障碍目的,获取作为标签代表该视图的节点信息。 返回AccessibilityNodeInfo
getLabeledBy() 为无障碍目的,获取作为标签代表该视图的节点信息。返回AccessibilityNodeInfo
getLiveRegion() 获取该节点的实时区域模式。一个实时区域是一个包含对用户来说重要信息的节点,且当其改变时应该告知用户。例如,在一个登录界面,有一个呈现“密码错误”通知的 TextView,该视图应该使用ACCESSIBILITY_LIVE_REGION_POLITE模式被标记为一个实时区域。这是无障碍服务的责任,控制TYPE_WINDOW_CONTENT_CHANGED 事件标识实时区域节点和其子元素的变化。实时区域模式,或如果视图不是个实时区域,返回ACCESSIBILITY_LIVE_REGION_NONE。
getMaxTextLength() 返回该节点的最大文本长度。
getMovementGranularities() 获取遍历该节点文本的移动粒度。
getPackageName() 获取该节点来自的包名。
getParent() 获取父级NodeInfo。
getRangeInfo() 如果节点是个范围,获取范围信息AccessibilityNodeInfo.RangeInfo
getText() 获取该节点的文本。
getTextSelectionEnd() 选择文本的末尾,如果没有选择文本,返回光标位置,或如果不存在选择文本和光标,返回-1。
getTextSelectionStart() 选择文本的起始,如果没有选择文本,返回光标位置,或如果不存在选择文本和光标,返回-1。
getViewIdResourceName() 获取源视图 id 的完全合格源名称。
getWindowId() 获取该信息来自的窗口的id。
hashCode() 返回该对象的哈希编码值。多用于判断node是否相同
isAccessibilityFocused() 获取该节点是否可被无障碍聚焦。
isCheckable() 获取该节点是否可选中。
isChecked() 获取该节点是否已选中。
isClickable() 获取该节点是否可点击。
isContentInvalid() 获取该节点的内容是否已失效。例如,一个日期数据格式不正确。
isDismissable() 获取该节点是否可被关闭。
isEditable() 获取该节点是否可编辑。
isEnabled() 获取该节点是否可用。
isFocusable() 获取该节点是否可获取焦点。
isFocused() 获取该节点是否已被聚焦。
isLongClickable() 获取该节点是否可长按点击。
isMultiLine() 获取该节点是否是个多行可编辑文本。
isPassword() 获取该节点是否是个密码。
isScrollable() 获取该节点是否可滚动。
isSelected() 获取该节点是否已被选择。
isVisibleToUser() 该节点是否对用户可见。
obtain(View root, int virtualDescendantId) 如果可获得,返回一个缓存实例,否则创建一个新的并设置源。 View:虚拟子树的根。 int:虚拟后代的id。返回一个实例化的AccessibilityNodeInfo
obtain(AccessibilityNodeInfo info) 如果可获得,返回一个缓存实例,或创建一个新的。返回的实例初始化自一个给定 root。
obtain() 如果可获得,返回一个缓存实例,否则,返回一个新的。
obtain(View source) 如果可获得,返回一个缓存实例,否则,返回一个新的并设置源。View:源视图。
performAction(int action, Bundle arguments) 在该节点上执行一个操作。int: 要执行的操作。Bundle:具有额外参数的包。返回一个布尔值,表示是否成功执行操作。
performAction(int action) 在该节点上执行一个操作。
recycle() 返回一个实例重复使用,回收掉当前的实例。
refresh() 刷新视图呈现的最新状态信息。返回一个布尔值,表示是否刷新成功。
removeAction(int action) 该方法在 API 级别 21 被弃用。
setAccessibilityFocused(boolean focused) 设置该节点可无障碍聚焦。
setBoundsInParent(Rect bounds) 在父级坐标中设置节点边界。
setBoundsInScreen(Rect bounds) 在屏幕坐标中设置节点边界。
setCanOpenPopup(boolean opensPopup) 设置该节点可打开一个弹窗或对话框。
setCheckable(boolean checkable) 设置该节点可选中。
setChecked(boolean checked) 设置该节点已选中。
setClassName(CharSequence className) 设置该节点来自的类。
setClickable(boolean clickable) 设置该节点可点击。
setCollectionInfo(AccessibilityNodeInfo.CollectionInfo collectionInfo) 如果该节点是个集合,设置集合信息。一个集合子集通常也是个集合项目。
setContentDescription(CharSequence contentDescription) 设置该节点的内容描述。
setContentInvalid(boolean contentInvalid) 设置节点内容无效。例如,一个日期数据格式不正确。
setContextClickable(boolean contextClickable) 设置该节点文本可点击。
setDismissable(boolean dismissable) 设置该节点可以被关闭。
setEditable(boolean editable) 设置该节点可编辑。
setEnabled(boolean enabled) 设置该节点可用。
setFocusable(boolean focusable) 设置该节点可聚焦。
setFocused(boolean focused) 设置该节点已聚焦。
setInputType(int inputType) 设置源中的输入类型为inputType
setLabelFor(View root, int virtualDescendantId) 为无障碍目的,设置可作为标签代表视图的信息。如果 virtualDescendantId 为NO_ID,根被设置为标签。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
setLabelFor(View labeled) 为无障碍目的,设置可作为标签代表视图的信息。
setLabeledBy(View label) 为无障碍目的,设置可作为标签代表视图的信息。
setLabeledBy(View root, int virtualDescendantId) 为无障碍目的,设置可作为标签代表视图的信息。如果 virtualDescendantId 为NO_ID,根被设置为标签。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
setLiveRegion(int mode) 设置该节点的实时区域模式。int:实时区域模式,或如果视图不是个实时区域时,为ACCESSIBILITY_LIVE_REGION_NONE。
setLongClickable(boolean longClickable) 设置该节点可长按点击。
setMaxTextLength(int max) 设置最大文本长度,或无限制时为-1。一般情况下,用来标识一个可编辑文本框有输入字符数目限制。
getMovementGranularities() 设置遍历节点文本的移动粒度。
setMultiLine(boolean multiLine) 设置该节点为一个多行可编辑文本。
setPackageName(CharSequence packageName) 设置该节点来自的包。
setParent(View root, int virtualDescendantId) 设置给定 root 虚拟后代的父元素。如果 virtualDescendantId 等于 NO_ID,该root 被设置为父级。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
setParent(View parent) 设置父元素。
setPassword(boolean password) 设置该节点为密码。
setRangeInfo(AccessibilityNodeInfo.RangeInfo rangeInfo) 如果该节点是个范围,设置范围信息。
setScrollable(boolean scrollable) 设置该节点可滚动。
setSelected(boolean selected) 设置该节点是否已选择。
setSource(View source) 设置信息源。
setSource(View root, int virtualDescendantId) 为给定 root 虚拟后代设置源。如果 virtualDescendantId 等于 NO_ID,该 root被设置为源。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
setText(CharSequence text) 设置该节点的文本。
setTextSelection(int start, int end) 设置文本选择的起始和结尾。
setViewIdResourceName(String viewIdResName) 设置源视图id的资源名称
setVisibleToUser(boolean visibleToUser) 设置该节点对用户可见。
toString() 返回一个代表对象的字符串。 getClass().getName() + '@' + Integer.toHexString(hashCode())
writeToParcel(Parcel parcel, int flags) 封装该对象到一个包中。
API 21 ADD
getError() API Level 21 获取该节点的错误文本。
setError(CharSequence error) API Level 21 设置该节点的错误文本。
getWindow() API Level 21 获取该节点所属的窗口。AccessibilityWindowInfo
removeAction(AccessibilityNodeInfo.AccessibilityAction action) API Level 21 移除一个可以在该节点上执行的操作。如果该操作未被添加到该节点,调用该方法没有任何效果。返回一个布尔值表示是否成功移除操作。
removeChild(View root, int virtualDescendantId) API Level 21 移除给定根节点的一个虚拟子元素。如果子元素不是之前被添加到该节点的,调用该方法无效果。 返回一个布尔值表示是否存在子元素。
removeChild(View child) API Level 21 移除一个子元素。如果子元素先前未被添加到该节点,调用该方法无效果。
API 22 ADD
getTraversalAfter() API Level 22 当前节点被访问后,获取无障碍遍历中的下一个节点。一个屏幕阅读器必须在访问该节点内容之前,访问其他节点的内容。如果存在,返回后续节点,否则,返回 null。
getTraversalBefore() API Level 22 当前节点被访问后,获取无障碍遍历中的上一个节点。屏幕阅读器必须访问该节点的信息,在访问先前节点内容之前。
setTraversalAfter(View view) API Level 22 设置无障碍遍历中下一个被访问的视图。一个屏幕阅读器在访问该节点内容之前,必须访问另一个节点的内容。
setTraversalAfter(View root, int virtualDescendantId) API Level 22设置无障碍遍历中下一个被访问的视图。一个屏幕阅读器在访问该节点内容之前,必须访问另一个节点的内容。如果 virtualDescendantId 等于 NO_ID,root被设置为前任。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
setTraversalBefore(View view) API Level 22 设置无障碍遍历中当前访问节点的上一个视图。一个屏幕阅读器在访问上一个节点之前,必须访问该节点的内容。
setTraversalBefore(View root, int virtualDescendantId) API Level 22 设置无障碍遍历中当前访问节点的上一个视图。一个屏幕阅读器在访问上一个节点之前,必须访问该节点的内容。如果virtualDescendantId 等于 NO_ID,root被设置为后任。一个虚拟后代是一个虚构视图,为无障碍目的,将自己报告为视图层次的一部分。该操作可以使绘制复杂内容的自定义视图,将自己报告为虚拟视图树,然后传递其逻辑结构。
API 23 ADD
isContextClickable() API Level 23 获取该节点是否上下文可点击。
API 24 ADD
setImportantForAccessibility(boolean important) API Level 24 设置该节点对无障碍很重要。
setDrawingOrder(int drawingOrderInParent) API Level 24 设置该节点相关视图的绘制顺序。
getDrawingOrder() API Level 24。获取该节点中视图的相对绘制顺序。绘制顺序只决定于节点的父级,所以该索引只与其兄弟姐妹相关。在一些情景下,绘制顺序是基本同步的,所以两个兄弟姐妹的返回值相同是可能的。返回值会被跳过也是可能的。
isImportantForAccessibility() API Level 24 返回该节点是否来源于一个对无障碍相当重要的视图。

performGlobalAction

Action 释义
GLOBAL_ACTION_BACK 相当于点击物理按键返回
GLOBAL_ACTION_HOME 相当于点击物理按键Home键
GLOBAL_ACTION_NOTIFICATIONS 相当于下滑打开通知
GLOBAL_ACTION_RECENTS 相当于点击物理按键最近任务键
GLOBAL_ACTION_QUICK_SETTINGS 打开快速设置
GLOBAL_ACTION_POWER_DIALOG 打开长按电源键的弹框

2.1.5 自定义AccessibilityService类的常规使用

public class MyAccessibilityService extends AccessibilityService {
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 1.AccessibilityEvent中一些常用的使用方法
        // 这里我们获取到该辅助功能的事件类型
        // 事件类型请参照 2.1.3中AccessibilityEventTypes表
        int eventType = event.getEventType();
        // 输出事件的字符串type
        String typeStr = event.eventTypeToString(eventType);
        // 根据事件类型来分发我们的需要的操作,这里以窗口变化为例
        if(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == eventType){
            // 判断我们的辅助功能是否在约定好的应用界面执行,以设置界面为例         if("com.android.settings".equals(event.getPackageName()){
                // doSomeThing
            }
        } else if(AccessibilityEvent.TYPE_GESTURE_DETECTION_START == eventType) {
            // 在监测到手势的时候
        } else {
            // 在完成操作时,可以关闭自己的服务,下次使用再次开启。
            // API > = 24
            disableSelf();
        }
        // 2.通过event来遍历我们的nodeInfo
        if (VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
            // 这里使用getResource()方法其实是从AccessibilityEvent继承的
            // AccessibilityRecord中抽取AccessibilityNodeInfo
            // 实际调用的是AccessibilityRecord中的方法,返回的是AccessibilityNodeInfo mSourceNode
            AccessibilityNodeInfo info = event.getSource();
        else
            info = getRootInActiveWindow();
        // 3.遍历info中的子节点
        if (info.getChildCount() != 0){
            // 通过一个循环将info的子节点遍历
            for (int i = 0; i < info.getChildCount(); i++) {
                // 获取子节点中某个特定的node,这里通过以下方法通过ID查找
                List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId("com.android" +".settings:id/xxxx");

                // 通过text查找
                List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByText("xxxx");
                // 打印nodeinfo的信息
                Log.e("InfoType: " + info.getClassName());
                Log.e("InfoText: " + info.getText());
                Log.e("InfoPkgName: " + info.getPackageName());
                Log.e("InfoViewId: " + info.getViewIdResourceName());
            }
        }

        // 4.为节点添加操作
        // 1) 首先获取到我们的节点
        AccessibilityNodeInfo info = event.getSource();
        // 2) 通过查找指定的ID、text来查找一个系列的节点,返回一个list,需要判断list.size()是否为空
        List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId("pkgName." + "id");
        AccessibilityNodeInfo info = list.get(0);
        // 3) 为节点添加操作,点击事件(事件可参照AccessibilityNodeInfo表)
 info.performAction(AccessibilityNodeInfo.ACTION_CLICK);

        // 4) 添加node可用的action,给node添加一个可清除焦点的操作
        // 通过addAction(AccessibilityNodeInfo.AccessibilityAction action)
        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_FOCUS);

        // 获取该node上可用的action属性list,可以用来查看该node的属性
        List<AccessibilityNodeInfo.AccessibilityAction> listAction = info.getActionList();
        // 5.获取该节点上子节点个数
        int childCount = info.getChildCount();
        // 6.操作完成后,回收实例,返回一个下次可用的实例
        info.recycle();
        // 需要注意的是,在node.performAction之后,调用本地广播的话,之后的globeAction不会起作用,例:
        sureStopNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        sureStopNode.recycle();
        SystemMessage.getInstance().send(SystemMessage.ACTION_POWER_BOOSTER_NEXT);
        performGlobalAction();// 这句话是不起作用的
        // 7.辅助功能的一些适用场景:
        // 1)部分应用中获取短信验证码(通过开启辅助功能的方式获取)
        //  - 监听通知栏的消息(typeNotificationStateChanged)
        //  - 弹出通知栏的时候获取该通知的节点信息(event.getSource())
        //  - 遍历root节点,取得显示信息的文本Node(info.findAccessibilityNodeInfosByViewId("pkgName." + "id"))
        //  - 通过NodeInfo.getText()方法获取到相应的文本信息,并取出验证码
        //  - 取消掉通知栏弹出框
        //  - 获取到要输入的EditText获取直接在本应用给需要填写验证码的区域设置文本。
        // 2)部分应用中恢复APP的初始设置(清除APP的数据)
        //  - 跳转到应用详情,通过findAccessibilityNodeInfosByViewId查找节点
        //  - 通过节点info.performAction(AccessibilityNodeInfo.ACTION_CLICK)点击清除
        //  - 弹出确定对话框,同上方式找到确定节点,点击后返回
        // 3)监测应用是否在前台,APP的启动(通过windowstatechange事件获取到当前的event的pkgName)
        //  - 通过接收typeWindowStateChanged事件,获取event的pkgName确定哪个应用启动或者在最上层显示(悬浮窗不适用)
        // 4)自动安装与卸载软件
        //  - 同3,寻找相应的节点,点击事件
        // 5)自动化UI测试
        // 6)最常见的抢红包
        // 7)通过辅助功能开启一些权限(不需要用户手动点击开启了)
        //  - 在用户确定需要开启权限时,自动跳转,寻找相应的开关按钮
        //  - 需要注意的是,部分开关(switch button, checkbox)可能没有ID,需要通过info.getClassName()来判断,属于那种类型的view。
    }

    @Override
    public void onInterrupt() {

    }
}

需要重写的一些方法:

  • onServiceConnected() 该方法在初始化辅助功能服务时调用,可以在这时做相应的初始化工作。
  • onAccessibilityEvent() 在获取到指定的监听事件时,通过回调这个方法来进行对应的操作
  • onInterrupt() 系统想要中断辅助功能时会调用该方法

当然service中的onUnbind()方法也同样适用,在onUnbind()中释放一些资源也是可以的。

Note: 这里需要了解的是辅助功能最重要的就是手机机型的适配。由于各个厂商的系统不一样,导致了我们在获取一些节点时,需要的节点ID不同(通过ID获取节点时一种比较高效的方式)。而我们一般通过Device Monitor中的Hierarchy View获取节点信息。同时在此方式下,很多手机没法获取到节点信息,我们就需要使用添加flagReportViewIds进行手动遍历界面节点的方式获取nodeInfo。

以上是辅助功能常规使用以及使用场景的介绍。下面为辅助功能的注册。

<service
    android:name=".我们自己继承自AccessibilityService的类"
        android:label="@string/app_name"
        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/第二个辅助功能配置方式的文件,存放在xml下"/>
</service>

[========]### 20 新增方法,接口(预览版)使用探究:

2.2.1 AccessibilityService新增方法一览:

API 21:

Method 描述
getWindows() 返回窗口最上层的一个用户可交互的窗口信息节点 List

获取屏幕上的窗口。
返回一个有目标的用户可以与之进行交互的窗口,而不是所有的窗口。

为了访问Windows,服务必须通过在其元数据中设置AccessibilityService_canRetrieveWindowContent属性来声明检索窗口内容的功能。此外,服务必须选择通过设置FLAG_RETRIEVE_INTERACTIVE_WINDOWS标志来检索交互式窗口。

使用此方法来获取我们屏幕上最上层的一个窗口根节点。

API 24:

Method 描述
getSoftKeyboardController() 返回软键盘控制器,可用于查询和修改软键盘显示模式。

返回AccessibilityService.SoftKeyboardController其中可以使用添加addOnShowModeChangedListener()的方式来进行对键盘的显示隐藏状态监听,通过getShowMode()获取到软键盘的显示隐藏状态,setShowMode(int showMode)设置软键盘的显示隐藏状态。

Method 描述
disableSelf() 关闭自己service的方法,在设置界面可以看到辅助功能状态被关闭

通过在服务中调用该方法可以将自己的服务停止,同时在辅助功能开启界面上的开关也会随之关闭。

Method 描述
dispatchGesture(GestureDescription gesture, AccessibilityService.GestureResultCallback callback, Handler handler) 将手势发送到触摸屏。

可以通过设定GestureDescription将手势的轨迹显示在触摸屏上,同时可以通过GestureResultCallback回调获得事件的完成情况。

Method 描述
findFocus(int focus) 找到具有指定焦点类型的视图。

这里的焦点类型有FOCUS_INPUTFOCUS_ACCESSIBILITY分别代表了输入焦点和无障碍重点。
FOCUS_ACCESSIBILITY这个焦点,是需要使用setAccessibilityFocused(boolean isFocus)方法进行设置过得焦点。

需要注意的是:如果要使用该方法的话,需要先设置AccessibilityService_canRetrieveWindowContent同时添加一个flag:FLAG_RETRIEVE_INTERACTIVE_WINDOWS

Method 描述
getMagnificationController() 返回放大控制器,可用于查询和修改显示放大的状态。

返回:AccessibilityService.MagnificationController。为了控制放大倍数,辅助功能服务必须通过在其元数据中设置AccessibilityService_canControlMagnification属性来声明该功能。

API 26:

Method 描述
getAccessibilityButtonController() 系统导航区域内的辅助功能按钮控制器

返回:AccessibilityButtonController

当设置FLAG_REQUEST_ACCESSIBILITY_BUTTON时,此类可用于查询辅助功能按钮的状态并注册回调以进行交互,并对可访问性按钮进行状态更改。

注意:此类和FLAG_REQUEST_ACCESSIBILITY_BUTTON不应该被用作通过AccessibilityService为用户提供功能的唯一手段。 一些设备实现可以选择不提供软件呈现的系统导航区域,使得该功能永久不可用。

注意:在支持辅助功能按钮的设备实现中,它可能始终不可用,例如前台应用程序使用SYSTEM_UI_FLAG_HIDE_NAVIGATION时。 用户还可以选择将该按钮分配给另一个可访问性服务或功能。 在每种情况下,将调用注册的AccessibilityButtonController.AccessibilityButtonCallback的onAvailabilityChanged(AccessibilityButtonController,boolean)方法来提供对可访问性按钮对注册服务的可用性的更改的通知。

注意底部导航栏的小人:

​ image

下面是一个简单的用例:

public class MyAccessibilityService extends AccessibilityService {
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        AccessibilityServiceInfo info = new AccessibilityServiceInfo();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            info.flags = AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
        } else {
            info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
        }
        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
        info.notificationTimeout = 100;
        setServiceInfo(info);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            AccessibilityButtonController mAccessibilityButtonController = getAccessibilityButtonController();
            mAccessibilityButtonController.registerAccessibilityButtonCallback(new AccessibilityButtonController.AccessibilityButtonCallback() {

                @Override
                public void onClicked(AccessibilityButtonController controller) {
                    super.onClicked(controller);
                    // 底部导航栏中辅助功能按钮点击事件回调
                    Toast.makeText(MyAccessibilityService.this, "on Click", Toast.LENGTH_SHORT).show();
                }
                @Override
                public void onAvailabilityChanged(AccessibilityButtonController controller, boolean available) {
                    super.onAvailabilityChanged(controller, available);
                    // 辅助功能可用性改变的回调。返回了辅助功能底部按钮是否可用的布尔值,和按钮控制器
                    // available = true 表示该按钮对本服务可用
                    // available = false 是由于设备显示了按钮,或按钮被分配到另一个服务或其他原因。
                }
            });
        }
    }

    @Override
    public void onInterrupt() {

    }

}

在开启辅助功能之后,如果是O系统的话,在底部导航栏会出现一个辅助功能的小人。同时这个小人可以接收到点击事件,和辅助功能可用状态变化。

Method 描述
getFingerprintGestureController() 返回指纹手势控制器

指纹手势控制器:FingerprintGestureController

只要设备具有能够检测手势的传感器,AccessibilityService可以捕获在设备的指纹传感器上执行的手势。
使用时需要声明CAPABILITY_CAN_CAPTURE_FINGERPRINT_GESTURES同时需要权限USE_FINGERPRINT,由于没有真机调试该方法,待Google发布正式版API后继续研究

支持的四个事件:

常量 描述
FINGERPRINT_GESTURE_SWIPE_DOWN 指纹手势下滑。
FINGERPRINT_GESTURE_SWIPE_LEFT 指纹手势左滑。
FINGERPRINT_GESTURE_SWIPE_RIGHT 指纹手势右滑。
FINGERPRINT_GESTURE_SWIPE_DOWN 指纹手势上滑。

公有方法

方法 描述
isGestureDetectionAvailable() 判断手势监测是否可用。
registerFingerprintGestureCallback(FingerprintGestureController.FingerprintGestureCallback callback, Handler handler) 通过该方法注册一个指纹手势的回调。
unregisterFingerprintGestureCallback(FingerprintGestureController.FingerprintGestureCallback callback) 取消注册。
registerFingerprintGestureCallback(FingerprintGestureController.FingerprintGestureCallback, Handler) 取消注册。

其中的FingerprintGestureController.FingerprintGestureCallback与之前的辅助功能按钮回调基本相同,都只有两个方法。

方法 描述
onGestureDetected(int gesture) 这里获取到相应的手势。
onGestureDetectionAvailabilityChanged(boolean available) 这里返回了手势监测是否可用。

2.2.1 AccessibilityNodeInfo新增方法一览:

API 21

Method 描述
getError() 获取该节点的错误文本。
setError(CharSequence error) 设置该节点的错误文本。

以上两个方法需要搭配使用。

Method 描述
getWindow() 获取该节点所属的窗口(根节点)

通过该方法,我们可以使用一个子节点来重新获取到我们的根节点。在需要多次操作界面元素的情况下(同时没有产生windowstatechange事件),我们可以不通过监听事件来进行下一个节点的操作。

Method 描述
addAction(AccessibilityNodeInfo.AccessibilityAction action) 为一个nodeInfo添加一个操作,API 21
removeAction(AccessibilityNodeInfo.AccessibilityAction action) 移除一个可以在该节点上执行的操作。
removeChild(View root, int virtualDescendantId) 移除给定根节点的一个虚拟子元素。
removeChild(View child) 移除一个子元素。

第一个addAction(AccessibilityNodeInfo.AccessibilityAction action)方法是API 21时替代API 20中的addAction(int action)方法。
提供了一个在辅助功能外操作节点的方法,该方法不能在辅助功能服务中调用,不然会报错java.lang.IllegalStateException: Cannot perform this action on a sealed instance.

后三个方法使用的前提都是需要事先在节点中添加了操作,或者视图,不然都不会起作用。要说明的是removeAction方法也需要在服务外调用,不然也要报错

API 22

Method 描述
getTraversalAfter() 当前节点被访问后,获取无障碍遍历中的下一个节点。
getTraversalBefore() 当前节点被访问后,获取无障碍遍历中的上一个节点。
setTraversalAfter(View view) 设置无障碍遍历中下一个被访问的视图。一个屏幕阅读器在访问该节点内容之前,必须访问另一个节点的内容。如果 virtualDescendantId 等于 NO_ID,root被设置为前任。
setTraversalAfter(View root, int virtualDescendantId) 设置无障碍遍历中下一个被访问的视图。
setTraversalBefore(View view) 设置无障碍遍历中当前访问节点的上一个视图。

通过在view的xml属性中添加android:accessibilityTraversalAfter="+id"方式来进行辅助功能遍历时的顺序后,通过本方法获取下一个节点。

通过在view的xml属性中添加android:accessibilityTraversalBefore="+id"方式来进行辅助功能遍历时的顺序,同上。

通过View.setAccessibilityTraversalBefore(+id)方式设置顺序。

API 23

Method 描述
isContextClickable() 获取该节点是否上下文可点击。

通过View的setContextClickable(Boolean isClickable)方法设置一个View是否可以点击。然后通过该方法,我们可以获取到节点上下文的可否点击状态。

API 24

Method 描述
setImportantForAccessibility(boolean important) 设置该节点对无障碍很重要。
isImportantForAccessibility() 返回该节点是否来源于一个对无障碍相当重要的视图。

通过View的XML属性设置android:importantForAccessibility=""状态后,我们可以通过本方法获取到该状态的boolean值。

Method 描述
setDrawingOrder(int drawingOrderInParent) 设置与此节点对应的视图的绘图顺序。
getDrawingOrder() 获取该节点中视图的相对绘制顺序。

同样的不能在辅助功能服务中调用该方法。
参考ViewGroup.setChildrenDrawingOrderEnabled())中children的绘制顺序理解。

返回与该节点对应的视图相对于其兄弟姐妹的绘图位置。
返回的数值从1开始,表示相对于父节点中,并列view的绘制顺序。

首页
关于博主
我的博客
搜索