← all articles

Installing a test-only APK with adb install -t

You run adb install app-debug.apk and get back Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]. The apk is fine, the phone is fine, the cable is fine. Android is refusing on purpose. This is the same question people keep asking on Stack Overflow (question 25274296), and the short answer is: add -t to the install command. The longer answer is worth knowing, because the flag exists for a reason and it changes how updates behave later.

This tutorial is for developers, QA people and anyone who ends up holding an apk built by someone else’s CI job and a phone on a desk. I run a lot of Android hardware in Singapore, and test-only builds turn up constantly, usually because a build server or an IDE produced them and nobody told the person doing the install. By the end you will know what the testOnly flag is, how to confirm an apk has it, how to install it, how to update it, and what to do when the install still fails.

I am assuming you own the app or are authorised to test it. Nothing here is about getting around protections on someone else’s software.

what you need

  • a computer with Android platform-tools installed (adb). the official download and the command reference are on the Android Developers adb page.
  • an android phone or emulator with developer options enabled and USB debugging switched on. if the toggle is greyed out, see fixing a greyed-out USB debugging toggle on Android.
  • a usb cable that carries data, not a charge-only one. wireless debugging works too if your device supports it.
  • the apk file itself. for the examples I use app-debug.apk and the package name com.example.myapp. swap in your own.
  • optional: aapt2 from Android SDK build-tools, if you want to inspect the manifest before installing.
  • cost: nothing, unless you need extra phones. see the scaling section.

step by step

1. understand what testOnly actually is

Android has a manifest attribute on the application element called android:testOnly. It is documented in the application element reference. When it is set to true, the package is marked as a test build. Android’s package installer then refuses to install it by normal means. Tapping the file in a file manager fails, adb install fails, and Google Play will not accept an upload with the flag set.

The point is to stop a half-finished build leaking out to real users. A test-only apk is meant to move from a developer or a CI system to a test device through a deliberate channel, and -t is that deliberate channel. It tells the package manager “yes, I know this is a test package, install it anyway.”

Where does the flag come from? Three places:

  • debug builds pushed by the Android Studio run button, which have been marked test-only in some setups
  • builds made against a preview SDK
  • apks where someone set android:testOnly="true" by hand in the manifest

Expected output for this step: none, it is background.

2. reproduce the error

Plug the phone in and check adb can see it.

adb devices

You should see your serial followed by device. Then try the plain install:

adb install app-debug.apk

On a test-only apk you will get:

Performing Streamed Install
adb: failed to install app-debug.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]

Some Android versions print a slightly different second line, but INSTALL_FAILED_TEST_ONLY is the part to look for. If it breaks: if adb devices shows unauthorized, unlock the phone and accept the RSA fingerprint prompt. If the list is empty, check the cable and USB mode before anything else.

3. confirm the apk is test-only before you change anything

You do not have to guess. If you have aapt2, dump the manifest:

aapt2 dump xmltree --file AndroidManifest.xml app-debug.apk | grep -i testOnly

A test-only apk prints a line like A: http://schemas.android.com/apk/res/android:testOnly(0x01010272)=true. No output means the attribute is not there, and your failure has a different cause (jump to common pitfalls).

You can do the same check against something already on the phone, which is handy later:

adb shell dumpsys package com.example.myapp | grep -i pkgFlags

If the flags list contains TEST_ONLY, the installed copy is a test build. If it breaks: if aapt2 is not found, it lives in build-tools/<version>/ inside your SDK folder and is not on your PATH by default. Call it by full path.

4. install with -t

Now the actual fix:

adb install -t app-debug.apk

Expected output:

Performing Streamed Install
Success

The -t flag is listed in the adb install options on the Android Developers page linked above as the option that allows test packages. If it breaks: if you still see INSTALL_FAILED_TEST_ONLY, check you typed -t before the apk path, and that you are not running an old adb. run adb version and update platform-tools if it is years old.

5. update an existing install with -r -t

Here is the part that catches people. If the test-only build is already on the phone and you push a newer one, you need both flags:

adb install -r -t app-debug.apk

-r replaces the existing app and keeps its data. -t is still needed because the new apk is still test-only. Drop either one and you will get a failure, either INSTALL_FAILED_ALREADY_EXISTS or the test-only error again, depending on the Android version.

Expected output is Success. If it breaks: if you get INSTALL_FAILED_UPDATE_INCOMPATIBLE, the signing key differs from the installed copy. Debug keystores differ between machines, so an apk built on a colleague’s laptop will not update one built on yours. Uninstall first with adb uninstall com.example.myapp, which wipes the app data, then install again.

6. install a split or multi-apk build

Modern builds often come as several files from bundletool or a Gradle split. Use install-multiple and keep the flag:

adb install-multiple -t base.apk split_config.arm64_v8a.apk split_config.en.apk

All the parts must come from the same build and be signed with the same key. If it breaks: INSTALL_FAILED_INVALID_APK or a missing split error almost always means you mixed parts from two different builds. Re-export the whole set.

7. launch it and verify

Start the app from the command line so you know it is really installed and runnable:

adb shell monkey -p com.example.myapp -c android.intent.category.LAUNCHER 1

Expected output is a short Events injected: 1 line and the app opening on screen. If you want to see it in a mirrored window instead of looking at the phone, scrcpy does that. see do you need a dedicated GPU to run scrcpy if you are wondering about the hardware cost. If it breaks: if monkey reports No activities found to run, the package name is wrong. list installed packages with adb shell pm list packages | grep myapp.

8. fix the build so you stop needing -t

If you own the project and the flag is unwanted, remove it at the source rather than living with -t forever. If an IDE run is stamping it on, you can tell the Android Gradle plugin not to, by passing this property to Gradle:

./gradlew assembleDebug -Pandroid.injected.testOnly=false

That is a property the Android Gradle plugin reads, and I have seen it work, but it is not a headline documented feature, so test it on your own toolchain and check the result with step 3. If the flag was hand-written in your manifest, delete the attribute. A release build you intend to ship should never carry testOnly, and Google Play will reject it if it does.

Expected output: a fresh apk where the step 3 check prints nothing. If it breaks: if the flag keeps coming back, grep your manifests, including the merged manifest under build/intermediates, because a library or a build variant overlay may be adding it.

9. clean up

When the test round is over, remove the app so the next tester starts clean:

adb uninstall com.example.myapp

Expected output is Success. If it breaks: if you get DELETE_FAILED_DEVICE_POLICY_MANAGER, the app is a device admin or protected by a work profile. Deactivate it in settings, then retry.

common pitfalls

  • using -t on the first install and forgetting it on updates. the updated apk is still test-only, so the update fails with the same error you thought you had fixed.
  • blaming the device. people reflash phones or hunt for hidden settings when the cause is a single manifest attribute. run the step 3 check first, it takes ten seconds.
  • confusing test-only with a version problem. if you are trying to go from a newer build to an older one, the failure is INSTALL_FAILED_VERSION_DOWNGRADE, and that is a different flag. see downgrading an installed Android app with adb install -d. note that -d does not work on every build, it depends on whether the app is debuggable.
  • hitting a vendor block that looks similar. on some Xiaomi and Redmi phones the failure is INSTALL_FAILED_USER_RESTRICTED, which is a MIUI security toggle and nothing to do with testOnly. the fix is on the phone, covered in fixing INSTALL_FAILED_USER_RESTRICTED on Xiaomi and Redmi phones.
  • handing a test-only apk to a non-technical tester. they will tap it, Android will refuse, and they will tell you the build is broken. either install it yourself with -t, or ship them a normal debug build without the flag.

scaling this

At 10 devices, a shell loop is enough. Pull the serials from adb devices and run the install against each one with -s:

for s in $(adb devices | awk 'NR>1 && $2=="device" {print $1}'); do
  adb -s "$s" install -r -t app-debug.apk
done

Run it sequentially at this size; it is easy to read and failures are obvious.

At 100 devices, sequential installs stop being acceptable, so run them in parallel, with xargs -P or a small script that logs one result line per serial. Log the exact failure string for each device. In a fleet you will see a mix of causes, for example a stale signing key on a few phones, unauthorized on others, a full storage error on the old ones. A single “install failed” count tells you nothing. Also watch the host. USB hubs and the machine itself become the limit well before adb does, which I cover in host limits when running many Android phones.

At 1000 devices, you are no longer typing adb commands at a desk. You need a device inventory, a queue of install jobs, retries with backoff, and a rule for what happens to a device that fails three times. The testOnly detail matters most here in your build pipeline. Make CI emit one artifact class for devices (test-only allowed, installed with -t) and a separate release class that is checked for the flag before it goes anywhere. Add the aapt2 check from step 3 as a pipeline gate so a stray flag fails the build instead of 400 installs.

If you do not want to own the phones at all, cloudf.one rents real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP, controlled from the browser. It is Singapore-only. One thing to be clear about: there is no self-service adb. adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request, by emailing [email protected]. So it suits you if you need real Singapore handsets for testing and are happy to arrange that tunnel first. if you only need a few local devices, a drawer of old phones is cheaper.

where to go next

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-06.

free download
The adb and scrcpy command cheat sheet

The commands we reach for on a rack of real phones, on one printable page. Leave your email and we will also tell you when we publish a new guide, a few times a month at most.

from the team behind cloudf.one
Passes on the emulator, fails on a real phone?

cloudf.one rents real Android phones in Singapore that you control from the browser, each on a persistent Singapore mobile IP. Real hardware, a real OEM build and a real carrier network, without buying and racking the phones yourself.

test on a real phone →
read on
More from ADB Handbook

adb commands, scrcpy flags, automation frameworks, and why a test passes on one phone and fails on the next.

browse all articles →