用三个方式跑同一个 IME 问题后,我看到的 AI Agent 在 Android 上的真实缺口

这篇文章选用 Compose IME 问题进行测试,用三条路径各跑了一遍,分别是 Codex,使用 Android CLI 的 Codex 和使用 Test Android Apps 的 Codex,模型选用的是 gpt-5.4。

Android CLI 是 Google 新出的工具,解决的是 Android 开发里那些特别容易散掉的 glue。

Test Android Apps 是 OpenAI 官方的插件,一套面向 emulator QA 和性能取证的工作流 skill。

这篇文章不回答"谁能修掉这个 bug"。现代 AI Agent 修这类小问题不难,三条路径最后都能搞定。文章想问的是:面对同一个 Android 问题,每条路径削减了哪类判断成本,哪条是在把已有的 capacity 收得更整齐。


环境设置

2026 年 4 月 16 日,Google 在 Android Developers Blog 发布了新的 Android CLI。

官方博客发布页
Android CLI overview 页面

三条路径都绑在同一个问题上:Compose 页面底部输入框和发送按钮,在键盘弹出后会不会被 IME 挤压,以及 agent 要怎么把这个问题查明、修掉、验证掉。

共享修复动作只有一个:

Kotlin
Modifier.navigationBarsPadding().imePadding()

路径一:不用 Android CLI,问题一样能做成

没有 Android CLI,AI Agent 也完全可以把这个 IME 问题查出来、修掉、验证掉。但代价是 agent 需要自己把很多 Android 专有胶水拼起来。

查官方做法

baseline 这边没有走泛搜索,也没有找二手博客。直接在 Google Chrome 里打开官方 Android Developers 页面,确认 Compose 下 IME 和 system bar inset 的推荐写法。

Chrome 里的官方 Android inset 文档

这一步对 Android 开发者很熟悉。但对 agent 来说,仍然意味着额外判断:哪个页面才是官方当前建议,哪些结果已经过时,这份写法能不能直接落到当前项目里。

手工拼工程链路

链路是:Gradle 构建、adb install -r 安装、adb shell am start 启动、uiautomator dump 拉 UI 树。放回 macOS Terminal 里重跑,截了窗口图。

手工链路的 Terminal 窗口

这张图对应的一个问题:工具是散的。

在这条路径里,agent 得自己处理 APK 路径、launcher activity、adb shell input 的时机,以及 uiautomator dump 拉回来的 XML 到底该怎么看。对 Android 开发者来说,这些操作本身不陌生;对 agent 来说,它们构成了一层额外的 Android 语义胶水。

坏状态怎么被证实

baseline 的坏状态原始截图。键盘弹出后,底部的 Send from bottom row 已经看不到了。

Baseline 坏状态:键盘弹出后底部动作消失

同时拉了一份 uiautomator XML。它能证明状态,但形状很典型:这次坏状态 XML 有 12,175 字节,节点 bounds 需要自己读、自己比、自己推断下一步。

这就是手工路径的核心特征:它没有缺能力,缺的是收束。

修复后也能被完整验证

修复后用同一条手工链路去跑。键盘依然打开,但按钮已经被抬到键盘上方,没有再被吃掉。

Baseline 修复后:按钮保留在键盘上方

没有 Android CLI 能不能把这个问题做成?能。

但 Android 开发里 agent 在什么地方最容易浪费判断成本?手工路径也给出了很清楚的答案:官方知识检索是开放的,APK 和 Activity 语义要自己补,UI 状态证明依赖原始 XML,视觉结果到下一步动作之间没有现成的桥。传统工具链对 agent 来说太原始。


路径二:Android CLI 收缩的是 Android 专有胶水

Android CLI 处理的还是同一个坏页面、同一个修复动作。不同的是 agent 拿到知识、项目语义和验证反馈的方式。

docs fetch:把平台知识直接送回当前工作流

Console
android docs search "imePadding compose textfield keyboard overlap"
android docs fetch kb://android/develop/ui/compose/system/keyboard-animations

返回的是修复真正需要的 Compose 片段:.imePadding().navigationBarsPadding()

Android CLI docs fetch 终端窗口

对应的真实修复代码在 Android Studio 里核对过,底部输入区容器确实用了 navigationBarsPadding()imePadding()

Android Studio 里的 IME 修复代码

手工路径里,agent 要自己离开当前上下文去搜官方页面;Android CLI 里,官方平台知识可以直接回到同一个终端工作流。docs fetch 把"查对官方建议"收成更窄的输入面。

describe / run:把项目语义从猜测变成接口

实际拿到的输出很短,但已经够说明问题:

Console
$ android describe --project_dir=artifacts/android-cli-flow/agent-cli-demo-visual
...
APK: /Users/baakarshan/Developer/labs/android-cli-demo/artifacts/android-cli-flow/agent-cli-demo-visual/app/build/outputs/apk/debug/app-debug.apk (Exists)
 
$ android run --device=emulator-5554 --apks=artifacts/android-cli-flow/agent-cli-demo-visual/app/build/outputs/apk/debug/app-debug.apk
App loaded: com.example.androidclivisualdemo
Multiple candidates found, picking the only launcher activity: com.example.androidclivisualdemo.MainActivity
Selected component: com.example.androidclivisualdemo.MainActivity
Installation completed successfully
Activation completed successfully

手工路径里,agent 要自己知道 APK 在 app/build/outputs/... 的哪个位置、launcher activity 应该怎么拼、当前设备上要怎么启动。Android CLI 的区别在于,它把这几个动作前面的 Android 项目语义先讲清楚,再去执行设备动作。它没有替代 Gradle,但它补上了 Gradle 前后那层 agent 最容易猜错的 Android 语义面。

只要项目进入多模块、多变体,describe 这种"先问项目,再执行动作"的方式就比手工猜路径稳得多。

layout --diff:把原始 UI dump 压成 agent 真能吃的变化摘要

如果只比较"能不能抓 UI 树",baseline 早就能用 uiautomator dump。真正让我觉得 Android CLI 开始像 agent 接口的,是 layout --diff --pretty

先让页面进入输入态,再让 Android CLI 比较变化前后的布局。它给的不是整坨 XML,而是更接近"下一步该怎么判断"的结果:哪些文本跑出屏幕了,哪些控件中心点变了,底部输入区移到了哪里。

Android CLI layout diff 终端窗口

手工路径里,拿到两份 XML,还要自己判断什么变化重要;Android CLI 里,先拿到一份面向变化的摘要,再决定要不要继续深挖。它没有替代原始信息源,但把原始信息塑形成了更适合下一轮推理的接口。

screen resolve:把视觉结果变成可执行动作

screen resolve 是这次最像"agent 工具"的部分。

实际跑到的结果:

Console
android screen resolve --screenshot=.../05-cli-fixed-before-ime-annotated.png --string='tap #133 then inspect #139'

返回:

Console
tap 539 2031 then inspect 539 2242

对应的真实窗口截图:

Android CLI screen resolve 终端窗口

这一步的价值很硬,补上了手工路径里最别扭的一环:手工路径里,截图是视觉证据,坐标还得靠 XML bounds 再翻一层;Android CLI 里,截图上的视觉编号可以直接回到动作坐标。它把"我看见了一个控件"推进成了"我下一步知道该怎么点它"。

skills:提前堵掉错误分支

Android CLI 还有第二层价值,来自 skills

选的是 edge-to-edge,因为它和前面的 IME 问题正好连得上,而且 android skills list 里确实能看到它。

没有再做"键盘有没有遮挡"的老问题,而是挑了一个更像真实 Android 开发里会踩到的坑:

  • 页面已经用了 Scaffold(contentWindowInsets = WindowInsets.safeDrawing)
  • 然后 agent 又直觉性地往内容容器上叠 navigationBarsPadding()imePadding()

这个分支危险的地方在于,它非常像一种"部分成功":键盘不再遮住控件了,构建过了,运行也正常,但 UI 质量其实还不对。

先看两种实现的源码对照。手工直觉修法在 Scaffold(WindowInsets.safeDrawing) 内又叠了一层 .imePadding()

Android Studio 里的 manual-naive 代码

按 skill 调整后的版本则保留 padding(innerPadding),并显式消费这层 inset:

Android Studio 里的 skill-guided 代码

手工直觉修法虽然让输入区留在了键盘上方,但留下了一大块没必要的空白区:

手工直觉修法的多余底部留白

按 skill 的建议改完后,这块多余空白被收掉了:

skill-guided 版本去掉多余留白

没有 skill,agent 很容易停在一个"看起来已经能用"的次优解上;有了 skill,这条错误分支会更早暴露出来。这种收益不体现在"能不能编译通过",而体现在"会不会为了同一块 UI 逻辑重新开第二轮探索"。


路径三:Test Android Apps 是 QA 和取证

还有一个 OpenAI 官方插件:Test Android Apps

它是一组把 Android emulator QA 和性能取证流程打包好的官方 skill。

实际验证到的有两层:android-emulator-qaandroid-performance。它们覆盖的是 Android CLI 主轴之外的另一块工作面。

android-emulator-qa:把 QA 链路标准化

复用同一个 IME 页面,严格按 QA skill 的顺序去跑:

Console
./gradlew :app:installDebug
adb shell cmd package resolve-activity --brief com.example.androidclivisualdemo
adb exec-out uiautomator dump /dev/tty > 01-before-tap.xml
python3 ui_tree_summarize.py 01-before-tap.xml 01-before-tap-summary.txt
python3 ui_pick.py 01-before-tap.xml "Type a message"
adb shell input tap 245 2031
adb shell input text test_android_apps
adb exec-out screencap -p > 02-ime-open.png
adb logcat -d > logcat-after-flow.txt

最值得看的不是"它也会 tap""它也会截图",因为这些能力 adb 本来就有。两步值得记:

  • ui_tree_summarize.py 先把原始 XML 压成可读提纲
  • ui_pick.py 再把目标节点翻成具体坐标

这次它算出来的输入框坐标是 245 2031,后面直接把这个坐标喂给 adb shell input tap

Test Android Apps QA 流程的 IME 打开状态

android-emulator-qa 把一条常见的 Android QA 链路标准化了:安装、resolve activity、启动、dump UI tree、总结 UI tree、从 UI tree 算坐标、截图、保存 logcat。它减少了测试取证里的临场 improvisation。

android-performance:证据采集

性能这层没有做成大而散的 profile,挑了一个很小的 flow:

  1. 启动 app
  2. reset gfxinfo
  3. 点输入框,打开 IME
  4. 输入一段文本
  5. 导出 gfxinfo
  6. 如果 preflight 允许,再跑一次短时 simpleperf

这次 preflight 给了一个很关键的前提:安装包是 DEBUGGABLE。所以 simpleperf record --app ... 跑通了。

前提本身能回指到原始输出:

Console
flags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]
pkgFlags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]

最后拿到的证据包括:

  • gfxinfo.txt
  • gfxinfo-framestats.txt
  • perf.data
  • simpleperf-self.txt
  • simpleperf-children.txt
  • simpleperf.csv

对应的运行态截图:

Test Android Apps 性能取证前的同一 IME flow

simpleperf 的代表行:前排主要是 RenderThread、ART interpreter 和 libc。

Console
Samples: 2695
14.92%    RenderThread                      ...  /apex/com.android.runtime/lib64/bionic/libc.so  read
13.28%    com.example.androidclivisualdemo  ...  /apex/com.android.art/lib64/libart.so           void art::interpreter::ExecuteSwitchImplCpp<false>(...)
3.30%     com.example.androidclivisualdemo  ...  /apex/com.android.art/lib64/libart.so           bool art::interpreter::DoCall<false>(...)

第一方 app 自己的 MainScreen 符号能看到,但占比很小:

Console
2.34%     0.00%   com.example.androidclivisualdemo  ...  com.example.androidclivisualdemo.ui.main.MainScreenKt.MainScreen
2.34%     0.00%   com.example.androidclivisualdemo  ...  com.example.androidclivisualdemo.ui.main.MainScreenKt.MainScreen$lambda$7

虽然 simpleperf 成功了,最热的 inclusive rows 仍然主要是 framework、ART interpreter 和 render thread;应用自有的行占很小一部分,比如 MainScreenKt.MainScreen

android-performance 这次证明的不是"插件帮我找到了一个重大热点"。它把 Android 性能证据采集写成了一套可重复的步骤;在 debug/profileable 条件成立时,它能真的把 simpleperf 跑起来。但采到证据,不等于自动得到一个强性能结论。好的取证工作流,和好的性能分析结论,不是同一件事。


结论

如果目标是做一个很小的 Android 任务,而且项目结构简单、你自己也很熟工具链,那么传统工具完全够用。代价主要是 agent 需要自己多补几层 Android 语义。

Android CLI 把 agent 在 Android 上最容易散掉的几段路径收成更像接口的东西:平台知识检索、APK 和组件语义、UI 变化判断、截图到动作的过桥。

Test Android Apps 补的则是另一块:如何把 emulator QA 和性能取证组织成更少即兴发挥的流程。

如果目标开始变成"让 agent 更稳定地参与真实 Android 工程",我会优先上 Android CLI。它改善的是最核心的四段链路:查官方建议、理解项目产物、判断 UI 变化、把视觉结果变成下一步动作。

如果你已经进入"我需要反复在 emulator 上做验证、留证据、采一轮性能数据"的阶段,那再叠加 Test Android Apps 才有意义。它把 QA 和取证工作流做的更标准。