网络热传App鉴定 |「李跳跳」里用到的无障碍权限是什么?

6,264 阅读10分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

「李跳跳」是什么?

u=2569031864,1078595779&fm=253&fmt=auto&app=138&f=JPEG.webp

一款用于跳过开屏广告的工具类应用。

其核心的原理,是借助Android系统上提供的无障碍功能,检测出当前界面上跳过按钮的位置,并自动帮我们完成点击的动作。

「无障碍」功能是什么?

解释这个概念之前,我们先来看几张图:

无障碍电梯

无障碍洗手间

无障碍坡道

以上这些都是在公园、地铁等的公共场所里常见的设施,目的是为了保障残疾人、老年人及其他行动不便者能安全、方便地通行,体现的是对于社会少数群体的关怀和尊重。

无障碍功能的设计初衷也是如此,同样是为了照顾到部分视力/听力受损、有认知障碍或无法完成精细动作的残障人士,使他们也能够和其他正常人一样,无障碍地使用手机应用来完成日常的沟通、学习和工作

除此以外,无障碍功能还可以弥补应用在某些使用场景上的限制,比如在烹饪的同时使用菜谱类的应用,有了无障碍功能的辅助,我们就可以使用语音指令而非触控手势来操控应用了。

无障碍功能

Android系统上提供的无障碍功能,实际可分为两个不同层面上的内容:

设计层面上,它要求应用界面上的元素:

  • 控件尺寸尽可能大,以方便用户点按

  • 色彩对比度尽可能高,以方便用户查看

低于建议的色彩对比度(左图)和足够高的色彩对比度(右图)

  • 尽可能包含能描述控件实际用途的说明文字

仅使用颜色创建界面元素的示例(左图),以及使用颜色、形状和文字创建界面元素的示例(右图)

操作层面上,它要求应用内提供的服务:

  • 尽可能为用户的每个操作提供及时、有效的反馈

  • 尽可能简化操作的步骤,或协助用户完成复杂操作

后者主要依赖平台级的「无障碍服务」(AccessibilityService)来实现。

「无障碍服务」能做什么?

Android系统提供了许多标准的无障碍服务实现,比如TalkBack这项服务,其提供了与屏幕交互时的实时语音反馈,对于盲人和视力低弱人士非常实用。

TalkBack

同时Android也允许开发者创建和分发自己的服务,以增强与用户的互动效果,打造运用范围更广的功能。

具体展开讲的话,自定义的无障碍服务通常包含对以下几个事项的处理:

收集信息

这里收集的主要是与界面交互相关的信息,包括所操作对象的类型、其包含的描述性文字以及其他详细信息。

我们知道,Android应用所采用的界面布局,是基于ViewViewGroup对象、以树状结构来进行构建的视图层级。

视图层次结构的图示,它定义了一个界面布局

Android系统正是基于此视图层级结构编写的无障碍事件,也即,它不仅会返回用户当前所交互的控件本身,还会返回包含此控件的父视图,以及此控件所包含的一系列子视图,这部分额外的信息统称为控件的上下文信息

上下文信息对于用户理解其所交互控件的含义至关重要。

以一个简单的复选框为例,对于一个有视力障碍的用户来说,如果无障碍服务仅仅反馈“是否选中”,而未能说明“选中的是什么内容”,用户就会感到很困惑。

有了完整的上下文信息,无障碍服务就能够为用户提供更加实用和有效的反馈。

对事件做出响应

这部分比较简单,通常就是确定无障碍事件的类型,并从触发此事件的视图节点中提取有用的文字描述,以文字转语音或振动的形式反馈给用户。

为用户执行操作

无障碍服务可以代替用户执行的操作,经过数个系统版本的迭代扩展,目前已经支持多达数十种,包括但不限于点击、长按、复制、粘贴、滚动、翻页等。

可以说,基本上,只要是Android平台上的标准UI控件(如TextView、RecyclerView、ViewPager等)内可支持的操作,都有对应的无障碍操作类型提供。

并且,这类操作不仅可以在应用内进行,还可以在系统全局进行,比如回到主屏幕、按“返回”按钮,以及打开最近使用的应用列表等,因此可以极大地简化用户与应用、系统之间的互动。

所以,说了这么多,「李跳跳」的「跳过开屏广告」功能到底是怎么实现的呢?

跳过开屏广告是怎么实现的?

其实,如果你能看到这里,此刻你的脑海里应该就已经有了大致的思路,下面,就让我们用代码来实际还原一下:

创建无障碍服务

首先我们要做的,就是创建一个扩展自AccessibilityService的类。

class MyAccessibilityService : AccessibilityService() {
...
}

由于无障碍服务本质上就是一个服务(Service),因此必须在AndroidManife应用清单文件中包含特定的声明。

在service元素的声明中,我们还必须添加一个intent过滤器,告诉系统这是一个无障碍服务。

此外,我们还需要添加 BIND_ACCESSIBILITY_SERVICE 权限,以确保只有系统才可以绑定到它。

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

配置无障碍服务

配置的目的是为了告诉系统,我们的无障碍服务运行的方式和时机、响应的事件类型、是针对特定的应用还是所有的应用以及采用的反馈类型等。

配置的方式有代码形式和XML文件形式两种,这里我们主要介绍后者。

首先,我们需要在res/xml目录下创建一个名为serviceconfig(命名可随意)的XML文件,其中的每一个键值对即表示一个配置选项,具体每一个配置选项的含义我们留到后面再介绍:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:canRetrieveWindowContent="true"
    />

随后,为了确保引用到该文件,我们需要在上面的服务声明中添加一个指向该XML文件的标记:

<service android:name=".MyAccessibilityService">
     ...
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

注册无障碍事件

这一步的作用就是确立我们的无障碍服务要处理的事件来自哪个应用的哪些事件类型,与此关联的几个配置选项就是:

  • android:packageNames(软件包名称):指定可接收的无障碍事件的应用来源。如果有多个应用需要指定,可用逗号进行分隔。而如果未指定,则默认接收来自所有应用的无障碍事件。

  • android:accessibilityEventTypes(事件类型):指定可接收的无障碍事件的事件类型。例如,accessibilityEventTypes="typeViewClicked|typeViewFocused"就表示可接收点击和获取焦点这两种事件类型,多个事件类型之间使用|进行分隔,typeAllMask则表示接收所有类型的事件。

此外,为了保证能获取无障碍事件来源控件的视图层次结构(即获取其父视图和子视图),我们还需添加canRetrieveWindowContent属性,并将其设为true,以请求相应的访问权限。

申请无障碍服务权限

只有当系统绑定到我们定义的无障碍服务,我们的服务才能正常运行起来,为此,我们需要跳转到系统的无障碍设置界面,开启我们安装的无障碍服务,代码如下:

startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))

以华为P30 pro为例,以上代码所跳转到的界面,对应的操作路径是「设置-辅助功能-无障碍」,随后,我们就可以从已安装的服务里找到我们自己的服务,然后点击开启按钮,我们的服务就会运行起来。

设置-辅助功能-无障碍.png

重载无障碍服务方法

除了常规的Service类方法外,无障碍服务还额外提供了2个必须重载的方法,分别是:

  • onAccessibilityEvent():当系统检测到与我们前面的配置选项相匹配的事件时,就会回调此方法,事件将以AccessibilityEvent类对象的形式提供。随后我们就可以解析该对象,并执行相应的处理。这个方法可能会在服务的整个生命周期内被调用多次。

  • onInterrupt():当系统要中断我们的服务正在提供的反馈时(通常是在控件焦点转移时),就会回调此方法。

获取事件详细信息

开头我们就讲了,「李跳跳」跳过开屏广告实际上就2步实现:

  1. 检测「跳过」按钮的位置
  2. 自动完成「点击」的动作

关于步骤1,我们要做的事情就是,在接收到无障碍事件后,使用getSource()从事件中检索出AccessibilityNodeInfo对象, 通过AccessibilityNodeInfo对象,我们可以浏览当前界面的视图层级结构,找到包含“跳过”字样文本的事件来源节点。

至于步骤2就很简单了,直接调用performAction(ACTION_CLICK)为用户执行点击操作即可。

完整代码如下:

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        val source = event.source ?: return
        for (i in 0 until source.childCount) {
            if (source.getChild(i)?.text?.contains("跳过") == true) {
                source.getChild(i).performAction(ACTION_CLICK)
            }
        }
        source.recycle()
    }

    override fun onInterrupt() {
    }
}

效果演示

可以看到,对比前一个未启用跳过开屏广告功能的网易云版本,后一个版本会在开屏广告显示的瞬间,就帮我们自动点击了跳过按钮,以达到跳过开屏广告的效果。

还想说点什么

好了,到这里我们就完成了一个“简易版”的李跳跳了,之所以说是“简易版”,是因为现在的李跳跳早已不限于跳过广告这一主要功能了,但大体上还是基于无障碍功能进行扩展的。

这也让我不得不慨叹,能力是有限的,但创意可以是无限的。Android开发团队或许也从未想到,无障碍功能居然还可以用来做这么多事情。实践之后,我也不得不对李跳跳开发者的创意拍手叫绝。

但另一方面,我也衍生出一种担忧,至于原因,显而易见,无障碍功能在解锁了相应的权限之后,其所能掌握的对于手机的控制权是相当可怕的

能读取屏幕内容,就可能窃取隐私;能操控用户行为,就可能违法犯罪。何况服务本身的特点就是在后台长时间运作,而不需要提供界面的。如果还允许这类应用在后台持续保活的话,真不敢想象它会在你意想不到的时候做出什么出格的事情。

当然我相信「李跳跳」的开发者的初心是纯粹的,毕竟多省下几秒看无聊广告的时间,就多几秒可以去做更有意义的事情,我只是表达对这种过度权力本身的担忧,毕竟就像罗翔老师说的:

同作为开发者,我佩服「李跳跳」的创意,但我敬畏这种权力。

少侠,请留步!若本文对你有所帮助或启发,还请:

  1. 点赞👍🏻,让更多的人能看到!
  2. 收藏⭐️,好文值得反复品味!
  3. 关注➕,不错过每一次更文!

===> 技术号:「星际码仔」💪

你的支持是我继续创作的动力,感谢!🙏