Skip to content

Commit f8e9c30

Browse files
committed
feat: add android snapshot helper
1 parent 5a3cf94 commit f8e9c30

25 files changed

Lines changed: 2002 additions & 6 deletions

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ jobs:
9090
- name: Run typecheck
9191
run: pnpm typecheck
9292

93+
android-snapshot-helper:
94+
name: Android Snapshot Helper Package
95+
runs-on: ubuntu-latest
96+
timeout-minutes: 15
97+
steps:
98+
- name: Checkout
99+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
100+
101+
- name: Setup toolchain
102+
uses: ./.github/actions/setup-node-pnpm
103+
104+
- name: Install Android SDK packages
105+
run: |
106+
yes | sdkmanager --licenses >/dev/null
107+
sdkmanager "platforms;android-36" "build-tools;36.0.0"
108+
109+
- name: Package Android snapshot helper
110+
run: pnpm package:android-snapshot-helper
111+
93112
integration-smoke:
94113
name: Integration Smoke
95114
runs-on: macos-26
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Release Android Snapshot Helper
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
workflow_dispatch:
8+
inputs:
9+
release_tag:
10+
description: GitHub Release tag to upload assets to, for example v0.13.3.
11+
required: true
12+
type: string
13+
checkout_ref:
14+
description: Optional branch, tag, or commit SHA to build. Defaults to the selected workflow ref.
15+
required: false
16+
type: string
17+
18+
permissions:
19+
contents: write
20+
21+
jobs:
22+
publish-android-snapshot-helper:
23+
name: Publish Android Snapshot Helper
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 30
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
with:
30+
ref: ${{ github.event.inputs.checkout_ref || github.ref }}
31+
32+
- name: Setup Node.js
33+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
34+
with:
35+
node-version: "22"
36+
37+
- name: Install Android SDK packages
38+
run: |
39+
yes | sdkmanager --licenses >/dev/null
40+
sdkmanager "platforms;android-36" "build-tools;36.0.0"
41+
42+
- name: Resolve release metadata
43+
id: meta
44+
run: |
45+
set -euo pipefail
46+
PACKAGE_VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version")"
47+
TAG_NAME="${{ github.event.release.tag_name || github.event.inputs.release_tag }}"
48+
VERSION="${TAG_NAME#v}"
49+
if [ "$VERSION" != "$PACKAGE_VERSION" ]; then
50+
echo "Release tag $TAG_NAME does not match package.json version $PACKAGE_VERSION" >&2
51+
exit 1
52+
fi
53+
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
54+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
55+
shell: bash
56+
57+
- name: Package Android snapshot helper
58+
id: package
59+
env:
60+
RELEASE_ASSET_DIR: ${{ github.workspace }}/.tmp/release-assets
61+
run: |
62+
set -euo pipefail
63+
mkdir -p "${RELEASE_ASSET_DIR}"
64+
sh ./scripts/package-android-snapshot-helper.sh \
65+
"${{ steps.meta.outputs.version }}" \
66+
"${{ steps.meta.outputs.tag }}" \
67+
"${RELEASE_ASSET_DIR}"
68+
shell: bash
69+
70+
- name: Upload helper assets to GitHub Release
71+
env:
72+
GH_TOKEN: ${{ github.token }}
73+
run: |
74+
set -euo pipefail
75+
gh release upload "${{ steps.meta.outputs.tag }}" \
76+
"${{ steps.package.outputs.apk_path }}" \
77+
"${{ steps.package.outputs.checksum_path }}" \
78+
"${{ steps.package.outputs.manifest_path }}" \
79+
--clobber
80+
shell: bash

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules/
22
.pnpm-store/
33
.fallow/
44
dist/
5+
.tmp/
56
.DS_Store
67
__pycache__/
78
*.pyc
@@ -23,8 +24,10 @@ xcuserdata/
2324
*.xcsettings
2425
*.xcresult
2526
*.ipa
27+
*.apk
2628
*.dSYM
2729
*.dSYM.zip
2830
*.app
2931
*.xctestrun
3032
*.xcarchive
33+
android-snapshot-helper/build/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<manifest
2+
xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.callstack.agentdevice.snapshothelper">
4+
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="36" />
5+
6+
<application
7+
android:debuggable="true"
8+
android:label="Agent Device Snapshot Helper"
9+
android:theme="@android:style/Theme.NoDisplay" />
10+
11+
<instrumentation
12+
android:name=".SnapshotInstrumentation"
13+
android:targetPackage="com.callstack.agentdevice.snapshothelper"
14+
android:label="Agent Device Snapshot Helper"
15+
android:functionalTest="true" />
16+
</manifest>

android-snapshot-helper/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Android Snapshot Helper
2+
3+
Small instrumentation APK used to capture Android accessibility snapshots without relying on
4+
`uiautomator dump`'s fixed idle wait behavior.
5+
6+
The helper is intentionally provider-neutral. Local `adb`, cloud ADB tunnels, and remote device
7+
providers can all install and run the same APK as long as they can execute ADB-style operations.
8+
9+
## Build
10+
11+
```sh
12+
sh ./scripts/build-android-snapshot-helper.sh 0.13.3 .tmp/android-snapshot-helper
13+
```
14+
15+
The build uses Android SDK command-line tools directly. It expects `ANDROID_HOME` or
16+
`ANDROID_SDK_ROOT` to point at an SDK with `platforms/android-36` and matching build tools.
17+
18+
## Run
19+
20+
```sh
21+
adb install -r -t .tmp/android-snapshot-helper/agent-device-android-snapshot-helper-0.13.3.apk
22+
adb shell am instrument -w \
23+
-e waitForIdleTimeoutMs 0 \
24+
-e timeoutMs 8000 \
25+
-e maxDepth 128 \
26+
-e maxNodes 5000 \
27+
com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation
28+
```
29+
30+
## Output Contract
31+
32+
The APK emits instrumentation status records using
33+
`agentDeviceProtocol=android-snapshot-helper-v1`.
34+
35+
Each XML chunk is sent with:
36+
37+
- `outputFormat=uiautomator-xml`
38+
- `chunkIndex`
39+
- `chunkCount`
40+
- `payloadBase64`
41+
42+
The final instrumentation result includes:
43+
44+
- `ok=true`
45+
- `helperApiVersion=1`
46+
- `waitForIdleTimeoutMs`
47+
- `timeoutMs`
48+
- `maxDepth`
49+
- `maxNodes`
50+
- `rootPresent`
51+
- `nodeCount`
52+
- `truncated`
53+
- `elapsedMs`
54+
55+
Failures return `ok=false`, `errorType`, and `message` in the final result.

0 commit comments

Comments
 (0)