Automated and Context-Aware Repair of Color-Related Accessibility Issues for Android Apps

1. 摘要

约 15% 的全球人口受到各种残障或视力障碍的影响,但许多移动端的用户体验(UX)设计师和开发者在开发 App 时并未重视可访问性问题。这意味着每七个人中就有一个用户在使用 App 时面临不平等的体验,这不仅影响用户,也可能违反相关法规。实际上,如果 App 开发时考虑可访问性,不仅能提升整体用户体验,还能提升商业价值。因此,已有不少研究和检测工具被提出用于识别可访问性问题。

然而,与检测相比,修复工作明显滞后,尤其是“颜色相关的可访问性问题”——比如文字对比度不足和图片对比度不佳,这类问题极大地影响了低视力用户和老年用户的使用体验,而当前的修复方法对此无能为力。

为此,我们提出了 Iris:一种自动化且具备上下文感知能力的方法,用于修复颜色相关的可访问性问题。该方法通过设计一致性的颜色替换策略和属性定位算法,在修复问题的同时保持 UI 风格的一致性。实验显示,Iris 可达到 91.38% 的修复成功率,且效率较高。用户调研也表明其结果令人满意,开发者反馈积极。我们在 GitHub 上提交的 40 个 Pull Request 中已有 9 个被合并,另有 4 个正在积极沟通后续修复。Iris 工具现已开源,旨在推动移动可访问性修复领域的进一步研究。

2. introduction

懒的讲了,感觉没啥用。反正大意就是解决这三个问题

1. 修复后必须保持原始 UI 页面的设计风格一致性;
2. 必须准确定位待修复的 UI 组件及其颜色属性;
3. 修复结果需被真实用户和开发者接受,具备实用性。

作者提出了 iris,可以弄一个流程图体现该作者的研究工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
                [1] APK 文件
↓
┌───────────────────────────────┐
│ Xbot 检测工具 │ ← 基于 Accessibility Scanner
└───────────────────────────────┘
↓
输出内容包括:
- 无障碍检测报告(JSON / XML)
- UI 页面截图(PNG)

┌────────────────────────────────────────┐
│ Iris 修复系统 │
└────────────────────────────────────────┘
↓
1. 颜色参考数据库(Reference DB)
- 收集 9978 个 App 的 UI 配色
- 用于推荐合适的替换颜色

2. 上下文感知颜色选择(Context-aware)
- 保持风格一致性(HSV 模型、色轮协调)
- 决定修改 foreground 还是 background

3. 属性定位模块(Attribute-to-Repair)
- 定位 XML 或 Java 中待修复组件
- 分析 textColor、background 等属性

4. 图像修复模块(仅针对图标)
- 判断功能性 vs 装饰性图像
- 调整颜色或替换 vector 图标

↓
┌───────────────────────────────┐
│ 修复后的 APK 输出 │
└───────────────────────────────┘

3. Preliminary

3.1 标准

需要解决以下问题:

  1. 普通文本要求前景色与背景色的对比度 ≥4.5:1*
  2. 大号加粗文本则要求 ≥3:1

3.2 颜色实现方式

在 xml 文件中,颜色一般是这样表达的

1
2
<TextView android:textColor="#80ff0000" />
<Button android:background="#80ff0000" />

提出 iris 用于修复这样的问题

3.3 检测工具提供的输入

这部分有很多,但是这个作者毫无创新,只是单纯的将各种工具整合而已(也有可能我不是这个方向的,不知道主流是什么样的^_^)

4. Approach

这块是 iris 的核心部分,主要流程如下

  1. 参考数据库构建(Reference DB Construction)
  2. 上下文感知的颜色选择(Context-aware Color Selection)
  3. 待修复属性定位(Attribute-to-Repair Localization)

4.1 Reference DB Construction

目的:提供颜色替换时的“设计参考”。

流程:

  • 使用 Xbot 对近 9978 个 APK 检测;
  • 收集其中未出现颜色对比度问题的 UI 组件;
  • 根据组件类型分类(如 Button、TextView 等);
  • 从 UI 截图中提取每个组件的前景色和背景色;
  • 使用** **getcolors() 获取最常出现的两种颜色;
  • 对比颜色值,保留满足对比度要求的颜色对,构建数据库。

优势:使用真实 App 中“被认可的配色”作为修复备选,提高了颜色风格的一致性。

4.2 Context-Aware Color Selection

算法流程在原文中给了,简单来说就是

  • 输入:当前问题颜色、颜色候选集、页面色调类型、偏转角;
  • 筛选候选颜色集,计算最小“色调距离”;
  • 如果没有合适候选色,就使用 HSV 微调获得备选颜色;
  • 返回最接近原设计的最优替换色。

4.3 Attribute-to-Repair Localization

根据 Xbot 报告,精准找到需要修改的 XML 属性或代码

定位方式:

  • 组件 ID(resource-id)存在时:可直接在反编译的 layout 中精确查找;
  • 仅有 bounds 坐标时:通过坐标在 layout tree 中找到组件,再通过文本等信息比对 XML 结构中的** **android:text属性,进行模糊定位。

5. 复现

前文我们知道了该论文有两部分组成,Xbot 和 iris 部分,因此我们一点一点来,先讲 Xbot 部分

5.1 Xbot

5.1.1 下载原代码

首先把 GitHub 仓库给 down 下来

仓库地址:https://github.com/tjusenchen/Xbot

这是一个很老的代码,需要用到 py2 来运行,mac 上是不能通过 conda 下载 py2 版本的,因此这般选择 pyenv(也是一种管理工具,和 conda 差不多),如果是 windows 用户的话可忽略

5.1.2 安装模拟器

模拟器也要和仓库中的 readme 中一样,选择 Android 7.1.1+PIX,如果太新的话会导致兼容性问题。

在创建好了模拟器之后,可以通过该命令检查是否连上了

1
adb devices

跳出来你的模拟器名字就 ok 了,如果你没看到的话,有很大原因是你没启动(我因为这个问题卡了一下午,md)

当然,使用的前提是你得先把 adb 下了,可以通过如下命令查看

1
2
3
4
5
adb version

Android Debug Bridge version 1.0.41
Version ...

系统默认一般是会带有的,路径如下

macOS:~/Library/Android/sdk/platform-tools/adb

Windows:C:\Users<用户名>\AppData\Local\Android\Sdk\platform-tools\adb`

如果发现没的话也问题不大,去官网下一个

官网地址:https://developer.android.com/studio/releases/platform-tools

然后解压添加到环境变量一条龙就 ok 了

5.1.3 安装 apk 文件

这部分就不用多说了,把 adb 命名下载好了之后就 adb install,例如

1
adb install path/to/your_app.apk

需要安装两个,分别是 GAS 和 Vol 的这两个 apk,都是在仓库里给了的,直接安装就行。

**尤其需要注意!!!**GAS 下载了之后,需要现在模拟器里设置- accessibility-GAS,打开服务。打开之后会跳出一个蓝色框框,然后再代码中修改你的这个悬浮窗的坐标,不然会造成闪退的情况!!!!!!修改的代码叫做 explore_activaity.py,scan_and_return 函数中

还有一个 vol 就没啥好说的了,下了就行,这是一个景点经典用 apk

5.1.4 运行 Xbot

1. 准备工作:

  1. Python 环境
  • 推荐使用** ****Python 2.7**
  • 所需库基本为标准库:如** os, commands, csv, shutil, **time
  1. Genymotion 模拟器准备
  • 已安装** ****Genymotion Desktop**
  • 启动一个设备(例如** **127.0.0.1:6555)
  • 模拟器 root 权限已开启(adb root 可运行)

2. 文件与路径结构检查

目录结构(假设在** **Xbot-main 下运行)

1
2
3
4
5
6
7
8
9
10
11
12
├── code/                      # Python 脚本主目录
│ ├── run_xbot.py # 主运行脚本
│ ├── explore_activity.py # 动态探索逻辑
│ ├── repkg_apk.py # 重打包逻辑(已用可选)
├── apks/ # 存放待测 APK 的目录
│ └── xxx.apk
├── config/ # 相关配置
│ ├── coolapk.keystore
│ ├── libs/
│ │ └── android-platforms/
├── results/ # 自动创建,输出目录
├── storydroid/ # 自动创建,用于参数记录等

3. APK 文件命名注意:

  • .apk 文件放在** **apks/ 中,**不要加中文名或特殊字符**
  • 如** **a2dp.Vol_133.apk

4. 代码准备工作

  1. 确认** **run_xbot.py 开头几项路径为你本机配置,例如
1
2
3
4
java_home_path = &#039;/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/&#039;
sdk_platform_path = &#039;/Users/yourname/.../android-platforms/&#039;
lib_home_path = &#039;/Users/yourname/.../libs/&#039;

  1. 清理旧 outputs(可选)
1
2
rm -rf results/ storydroid/

5. 执行命令

1
2
3
4
5
6
7
cd Xbot-main/code

python run_xbot.py 127.0.0.1:6555 ../main-folder/apks/

# adb
adb devices. # 查看设备名字

6. 运行 xml 修复

Xbot 只有原始 apk 页面的 Xbot,还需要通过 issues 来生成新的 xml 以让 iris 知道哪个组件有问题

1
2
3
python3 Xbot-main/code/txt2irisxml.py \
Xbot-main/results/outputs/a2dp.Vol_133/issues/a2dp.Vol.EditDevice/a2dp.Vol.EditDevice.txt \
Xbot-main/results/outputs/a2dp.Vol_133/issues/a2dp.Vol.EditDevice/AccessibilityScanner.xml

贴出 txt2irisxml 代码,放在 code 目录下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

# -*- coding: utf-8 -*-
import os
import sys
import xml.etree.ElementTree as ET

def parse_txt(txt_path):
with open(txt_path, &#039;r&#039;, encoding=&#039;utf-8&#039;) as f:
lines = [line.strip() for line in f.readlines() if line.strip()]

issues = []
i = 0
while i &lt; len(lines):
line = lines[i]

# --- Item label / Missing label
if &quot;Item label&quot; in line:
i += 1
res_id = lines[i] if i &lt; len(lines) else &quot;unknown&quot;
issues.append({
&quot;Type&quot;: &quot;MissingLabel&quot;,
&quot;ResourceID&quot;: res_id,
&quot;Class&quot;: &quot;android.widget.EditText&quot;
})
i += 1

# --- Text contrast
elif &quot;Text contrast&quot; in line:
i += 1
res_id = lines[i] if i &lt; len(lines) else &quot;unknown&quot;
i += 1
ratio = &quot;N/A&quot;
if i &lt; len(lines) and &quot;contrast ratio is&quot; in lines[i]:
try:
ratio = lines[i].split(&quot;contrast ratio is&quot;)[1].split(&quot;.&quot;)[0] + &quot;.&quot; + lines[i].split(&quot;contrast ratio is&quot;)[1].split(&quot;.&quot;)[1][:2]
except:
pass
issues.append({
&quot;Type&quot;: &quot;LowTextContrast&quot;,
&quot;ResourceID&quot;: res_id,
&quot;Class&quot;: &quot;android.widget.TextView&quot;,
&quot;ContrastRatio&quot;: ratio
})
i += 1

# --- Touch target too small
elif &quot;Touch target&quot; in line:
i += 1
res_id = lines[i] if i &lt; len(lines) else &quot;unknown&quot;
issues.append({
&quot;Type&quot;: &quot;TouchTargetTooSmall&quot;,
&quot;ResourceID&quot;: res_id,
&quot;Class&quot;: &quot;android.widget.Button&quot;
})
i += 1

else:
i += 1

return issues

def write_xml(issues, output_path):
root = ET.Element(&quot;AccessibilityIssues&quot;)

for issue in issues:
item = ET.SubElement(root, &quot;Issue&quot;)
ET.SubElement(item, &quot;Type&quot;).text = issue.get(&quot;Type&quot;, &quot;Unknown&quot;)
ET.SubElement(item, &quot;ResourceID&quot;).text = issue.get(&quot;ResourceID&quot;, &quot;unknown&quot;)
ET.SubElement(item, &quot;Class&quot;).text = issue.get(&quot;Class&quot;, &quot;android.view.View&quot;)
if &quot;ContrastRatio&quot; in issue:
ET.SubElement(item, &quot;ContrastRatio&quot;).text = issue[&quot;ContrastRatio&quot;]

tree = ET.ElementTree(root)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
tree.write(output_path, encoding=&quot;utf-8&quot;, xml_declaration=True)
print(f&quot;✅ Created XML: {output_path}&quot;)

if __name__ == &quot;__main__&quot;:
if len(sys.argv) != 3:
print(&quot;Usage: python txt2irisxml.py input.txt output.xml&quot;)
sys.exit(1)

txt_file = sys.argv[1]
xml_file = sys.argv[2]
issues = parse_txt(txt_file)
write_xml(issues, xml_file)

7. 运行结果

在/results/apk_name 中。主要需要的是 outputs 这个文件夹

5.2 IRIS

5.2.1. 解压 zip 文件

1
2
3
cd /root
unzip iris-mobile-master.zip
mv iris-mobile-master iris-mobile

同时需要将之前 Xbot 的输出 outputs 存放在根目录下,zip 是原始在 GitHub 中下载的

GitHub 地址:https://github.com/tjuyuxinzhang/iris-mobile

  1. 复制 Xbot 输出和 APK 到 IRIS 指定位置
1
2
3
4
5
6
mkdir -p /root/iris-mobile/code/data/xbot_output
cp -r /root/outputs/a2dp.Vol_133 /root/iris-mobile/code/data/xbot_output/

mkdir -p /root/iris-mobile/apks
cp /root/outputs/a2dp.Vol_133.apk /root/iris-mobile/apks/

5.2.2 修改一些路径代码

  1. repair_repack_class.py
1
2
3
results_folder = &quot;/root/iris-mobile&quot;
resultPath = &#039;/root/iris-mobile/refDB&#039; # 若没有这个文件夹可先建空目录
outputsPath = &#039;/root/outputs&#039;
  1. harmonizationTs2.py

把 250 行的 plt 部分注释了(如果用的是虚拟机,无图形页面的话)

5.2.3 切换目录并运行 IRIS 修复脚本

1
2
cd /root/iris-mobile/code
python2 repair_repack_class.py --app_name a2dp.Vol_133

5.2.4 运行结果

需要包含这些东西

  1. decompiling
1
2
3
4
5
6
7
8
9
10
11
12
13
decompiling...
I: Using Apktool 2.4.1 on a2dp.Vol_133.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /root/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
act_T_Alpha
  1. color layout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id_bound_colorSet
{&#039;Path&#039;: ([&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5, &#039;ManageData&#039;, &#039;&#039;, &#039;Text contrast&#039;), &#039;Action:&#039;: ([&#039;#F7ECEE&#039;, &#039;#F9EEF0&#039;], 3, &#039;CustomIntentMaker&#039;, &#039;TextView&#039;, &#039;Text contrast&#039;), &#039;title&#039;: ([&#039;#2979FF&#039;, &#039;#F9EEF0&#039;], 4.5, &#039;Preferences&#039;, &#039;&#039;, &#039;Text contrast&#039;), &#039;Data:&#039;: ([&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3, &#039;CustomIntentMaker&#039;, &#039;TextView&#039;, &#039;Text contrast&#039;), &#039;empty&#039;: ([&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3, &#039;ProviderList&#039;, &#039;&#039;, &#039;Text contrast&#039;), &#039;Type:&#039;: ([&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3, &#039;CustomIntentMaker&#039;, &#039;TextView&#039;, &#039;Text contrast&#039;), &#039;Output&#039;: ([&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5, &#039;ManageData&#039;, &#039;&#039;, &#039;Text contrast&#039;), &#039;pi_tv_name&#039;: ([&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3, &#039;PackagesChooser&#039;, &#039;&#039;, &#039;Text contrast&#039;)}
waitChangeColor_self
{&#039;pi_tv_name&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;title&#039;: (&#039;#2153AE&#039;, [&#039;#2979FF&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;Data:&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;Output&#039;: (&#039;#504348&#039;, [&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;Action:&#039;: (&#039;&#039;, [&#039;#F7ECEE&#039;, &#039;#F9EEF0&#039;], 3), &#039;Type:&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;Path&#039;: (&#039;#504348&#039;, [&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;empty&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3)}
colorToChange_self
{&#039;Path&#039;: &#039;#504348&#039;, &#039;title&#039;: &#039;#2153AE&#039;, &#039;Data:&#039;: &#039;#585858&#039;, &#039;empty&#039;: &#039;#585858&#039;, &#039;Type:&#039;: &#039;#585858&#039;, &#039;Output&#039;: &#039;#504348&#039;, &#039;pi_tv_name&#039;: &#039;#585858&#039;}
[&#039;#F7ECEE&#039;, &#039;#F9EEF0&#039;]
[]
colorToChange_Layout
{&#039;Path&#039;: &#039;#504348&#039;, &#039;title&#039;: &#039;#2153AE&#039;, &#039;Action:&#039;: &#039;#000000&#039;, &#039;Data:&#039;: &#039;#585858&#039;, &#039;empty&#039;: &#039;#585858&#039;, &#039;Type:&#039;: &#039;#585858&#039;, &#039;Output&#039;: &#039;#504348&#039;, &#039;pi_tv_name&#039;: &#039;#585858&#039;}
imageId_Name
{}
{(&#039;Data:&#039;, &#039;TextView&#039;): &#039;[0,332][263,436]&#039;, (&#039;Path&#039;, &#039;TextView&#039;): &#039;[0,524][532,575]&#039;, (&#039;Type:&#039;, &#039;TextView&#039;): &#039;[0,453][263,557]&#039;, (&#039;pi_tv_name&#039;, &#039;TextView&#039;): &#039;[173,1610][954,1741]&#039;, (&#039;Action:&#039;, &#039;TextView&#039;): &#039;[0,211][263,315]&#039;, (&#039;title&#039;, &#039;TextView&#039;): &#039;[42,1756][626,1794]&#039;, (&#039;empty&#039;, &#039;TextView&#039;): &#039;[53,263][1027,601]&#039;, (&#039;Output&#039;, &#039;TextView&#039;): &#039;[465,575][614,669]&#039;}
{}
[]
{&#039;Path&#039;: &#039;#504348&#039;, &#039;[0,453][263,557]&#039;: &#039;#585858&#039;, &#039;title&#039;: &#039;#2153AE&#039;, &#039;[0,332][263,436]&#039;: &#039;#585858&#039;, &#039;empty&#039;: &#039;#585858&#039;, &#039;[0,211][263,315]&#039;: &#039;#000000&#039;, &#039;Output&#039;: &#039;#504348&#039;, &#039;pi_tv_name&#039;: &#039;#585858&#039;}
waitChangeColor_self
{&#039;pi_tv_name&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;title&#039;: (&#039;#2153AE&#039;, [&#039;#2979FF&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;Data:&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;Output&#039;: (&#039;#504348&#039;, [&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;Action:&#039;: (&#039;&#039;, [&#039;#F7ECEE&#039;, &#039;#F9EEF0&#039;], 3), &#039;Type:&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3), &#039;Path&#039;: (&#039;#504348&#039;, [&#039;#817278&#039;, &#039;#F9EEF0&#039;], 4.5), &#039;empty&#039;: (&#039;#585858&#039;, [&#039;#FFFFFF&#039;, &#039;#F9EEF0&#039;], 3)}
{}
recompile...
(&#039;time cost&#039;, 327.10186195373535, &#039;s&#039;)

5.2.5 修复后的 apk 文件

1
/root/iris-mobile/repackaged/a2dp.Vol_133.apk

5.2.6 将其放在 mac 上的 apk 文件夹中,签名下载运行(Xbot 再次比对)

  1. 签名 apk
1
2
3
4
5
6
7
/Users/qijiazhou/Library/Android/sdk/build-tools/36.0.0/apksigner sign \
--ks ~/Desktop/test-key.jks \
--ks-key-alias testkey \
--ks-pass pass:123456 \
--key-pass pass:123456 \
--out ~/Desktop/apk/a2dp.Vol_133_signed.apk \
~/Desktop/apk/a2dp.Vol_133.apk
  1. 安装测试
1
adb install -r ~/Desktop/apk/a2dp.Vol_133_signed.apk

接着就是再运行 Xbot 的过程,将其放入 main- folder/apks 文件夹中

6. Experiments

用户调查就不说了,按照复现流程走下来应该是能一样的。当然,此处可以讲一下修复成功率计算公式评价指标

1
Repire = 修复成功的问题数/该app原始问题数 * 100%

站内搜索

没有找到内容!