Skip to content

Android: ColorOS blocks the test-only snapshot helper as a "PC install attack" (-99), removing all snapshot-based commands #2364

Description

@AliceLJY

Summary

On ColorOS (OPPO), the bundled Android snapshot helper cannot be installed. The device's package installer classifies the install as a "PC install attack" and fails the session with vendor code -99, so every snapshot-dependent command is unusable on these devices.

This is not a blanket adb-install restriction: normal (non-test-only) APKs install over the same adb connection without any prompt. The block is specific to APKs carrying android:testOnly="true", which the snapshot helper does.

Filing this because #1284 / #1285 deliberately chose hard-fail over the stock-UIAutomator fallback. That decision is sound, but it means an OEM that blocks test-only packages removes the entire Android capture surface with no supported escape hatch.

Environment

agent-device 0.20.10 (global npm install)
Host macOS 27.0, Node 22+
Device OPPO Find N6 (PLP110), ColorOS, targetSdk=36, physical device over USB
adb 1.0.41 / 37.0.1

Screen was on and unlocked throughout. USB debugging authorized (adb devices reports device, not unauthorized).

Reproduction

$ agent-device snapshot -i --json
Android snapshot helper failed: Failed to install Android snapshot helper:
failed to install .../agent-device-android-snapshot-helper-0.20.10.apk:
Failure [INSTALL_FAILED_TEST_ONLY]

Installing the same artifact by hand reproduces it and shows the vendor code:

$ adb install -r  <helper.apk>     # no -t
Failure [INSTALL_FAILED_TEST_ONLY]

$ adb install -r -t <helper.apk>   # with -t
Failure [-99]

$ adb push <helper.apk> /data/local/tmp/ && adb shell pm install -r -t /data/local/tmp/helper.apk
Failure [-99]

Root cause (logcat)

E OplusCustomizePackageManagerService: getAdbInstallUninstallDisabled start
D PackageManager: send package verification broadcast
D PackageManager: installStage allowInterceptAdbInstallInInstallStage pkg:com.callstack.agentdevice.snapshothelper
I PackageInstaller: PC install attack detected!com.callstack.agentdevice.snapshothelper
D PackageInstallerSession: Marking session as failed: -99
D PackageInstallerSessionExtImpl: uploadInstallFailInfos:{code=20010,
   initiatingPackageName=com.android.shell, targetSdk=36,
   app_pkg=com.callstack.agentdevice.snapshothelper, statusCode=-99}

-99 is not an AOSP PackageManager.INSTALL_FAILED_* constant; it comes from OPPO's interception layer (OplusCustomizePackageManagerService, allowInterceptAdbInstallInInstallStage).

What I ruled out

Hypothesis Test Result
Corrupt/truncated APK unzip -t on the bundled artifact Passes, no errors
Screen locked dumpsys window during install mScreenOn=true, mScreenLocked=false
AOSP adb-install verification Set verifier_verify_adb_installs 1 → 0, retry Still -99 (restored to 1 afterwards)
adb installs blocked wholesale Pulled an already-installed, release-signed app off the device and reinstalled it with adb install -r Success

That last row is the decisive one: the same adb channel installs a normal APK fine. Only the testOnly flag triggers the vendor block.

What still works without the helper

Verified on the same device with a session open: open / close, install / reinstall, screenshot (produced a 1140x2616 PNG), logs, apps. Only snapshot and everything built on it (click, fill, find, wait, diff) are unavailable.

Suggestions

  1. Document it. A line in the Android installation docs — "some OEM builds (ColorOS observed) reject test-only packages from adb; the snapshot helper cannot be installed on those devices" — would save the diagnostic time. The current hint from fix(android): actionable error when the snapshot helper is unavailable (#1284) #1285 is already good at saying what failed; naming this specific OEM class would say why.
  2. Consider an opt-in non-test-only helper build. If the helper did not set android:testOnly="true", it would install on these devices. I understand the flag is protective (it keeps the helper off Play and out of normal install flows), so this would need to be an explicit, clearly-labelled variant rather than the default.
  3. Consider an env var for a user-supplied helper artifact. adbProvider.snapshotHelperArtifact already exists for cloud providers; exposing an equivalent for local runs would let affected users supply a locally-repackaged helper without patching node_modules. I checked the AGENT_DEVICE_* surface and did not find one.

Happy to test any of these on the device — I have it available.

Activity

  1. thymikee commented on Sep 14, 2026

    @thymikee
    Member

    Thanks for the thorough report, the ruled-out table saved us real time.

    Good news: testOnly was a leftover from when the helper was debuggable, not something the helper needs. Instrumentation can only be started from shell via am instrument, and the session server binds loopback, so the flag protects nothing here. That means we can drop it (and the -t install flag) outright instead of adding a variant or an escape hatch. #2603 does that for both the snapshot and IME helpers. On a stock API 36 emulator both helpers now install with plain adb install -r, and snapshot/click/fill run end to end through the daemon.

    We don't have a ColorOS device, so you're the only one who can close the loop. Would you be up for building from that branch and trying it on the Find N6?

    git fetch origin fix/android-helpers-drop-test-only && git checkout fix/android-helpers-drop-test-only
    pnpm install && pnpm build && pnpm build:android
    adb install -r android/snapshot-helper/dist/agent-device-android-snapshot-helper-*.apk   # no -t
    node bin/agent-device.mjs snapshot -i --platform android

    If adb install still returns -99, the same logcat lines you pasted (PackageInstaller: PC install attack detected) would tell us whether the check also trips on the debug signing certificate, which is the one remaining unknown and would give us a concrete next lever. If you'd rather not set up the Android build tools, say so and we'll share a prebuilt APK from the branch.

  2. AliceLJY commented on Sep 15, 2026

    @AliceLJY
    Author

    Confirmed working on the Find N6 (ColorOS). Thanks for the quick turnaround.

    One thing that saves the build step: the fix shipped in npm 0.21.3 (published ~30 min after #2603 merged), and the published package ships the helper APKs directly under android/*/dist/, so there's no need to build from the branch. I verified the released artifact before installing:

    • agent-device-android-snapshot-helper-0.21.3.manifest.json -> "installArgs": ["install", "-r"], no -t
    • aapt2 dump xmltree on both the snapshot and IME helper APKs: no testOnly attribute

    Result after npm i -g agent-device@latest (0.20.10 -> 0.21.3):

    • adb -s <serial> install -r <snapshot-helper.apk> (no -t): no more -99, no "PC install attack" line in logcat
    • snapshot -i --json -> success: true, androidSnapshot.backend: "android-helper", helperVersion 0.21.3, helperTransport: "persistent-session", snapshotQuality: {state: "healthy"}, 315 nodes in 281 ms
    • Re-running it is instant and non-interactive: installReason: "current", ~1 s wall clock
    • The IME helper installs fine too: adb install -r <ime-helper.apk> -> Success, and installing it does not enable it (settings get secure enabled_input_methods still lists only my normal IME). I did not exercise fill itself, since activating the helper IME is device-global and this is my daily phone — but the install half of that path is no longer blocked either.

    One finding you can't see on an emulator, in case it's worth a line in the docs or the timeout hint: on ColorOS the first install of a given package still opens the system PackageInstallerActivity flow on the device, and it does so per package — both helpers needed it. It is two taps rather than one: confirm the install, then dismiss the completion screen.

    The very first snapshot -i returned adb timed out after 30000ms with empty stdout/stderr and adbFailure: "timeout". That was adb waiting on the dialog, not a rejection — dumpsys window showed the focus sitting on com.android.packageinstaller/.PackageInstallerActivity. Once it is dismissed the install completes and every subsequent run is instant. The IME helper returned a plain Success only because the screen was being watched and both taps happened immediately — which also means I cannot tell you which of the two unblocks adb install, since both landed well inside the 30 s window.

    So an unattended first run will stall there until it times out, and the current "adb server may be wedged / run adb kill-server" hint points away from the real cause in this case. Something like "if this is the first install of this helper, check the device for a system install-confirmation dialog" would have saved me a round of guessing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions