APK won't install on Android: every message explained
Why won't my Unity APK install on my phone?
Android refuses a Unity build for one of a short list of reasons, and each prints a different sentence: the app you tapped the file in has no install permission, an old copy signed with a different key is still on the phone, Minimum API Level or ARM64 is set past what the device has, or the file arrived truncated. Run adb install yourself, read the Failure line, then compare the phone's API level and abi against Player Settings.
Unity reports Build completed, and the phone refuses. That is not one problem but six, and each of them announces itself with a different sentence. Below is every way Android can say no to a Unity build, what the sentence actually means, and the one change that clears it.
Before installing: Unknown sources
Android 8 removed the single Unknown sources switch and replaced it with a permission granted per app. There is no toggle anywhere that allows everything — you give the right to install to the one app you actually tapped the file with.
- 1
Open and pick the app you tapped the file in — Chrome, Files by Google, Zalo — not the one you assumed. Then , and turn on Allow from this source.
- 2
MIUI and HyperOS put a second screen on top of that one: it asks for a signed-in Mi account, counts down while it scans, and when the scan dislikes the file the only message you get is Declined due to system restrictions. Turning off Scan apps for viruses inside the Security Center app is what removes that refusal.
- 3
Samsung runs a Play Protect scan instead: Google Play, your profile picture, Settings, Notifications, then turn off Scan apps with Play Protect. One thing no setting on the phone beats is a work profile — a device enrolled in a company account blocks sideloading by policy.
- 4
Installing from a computer over USB needs its own switch, Install via USB, in Developer options — and MIUI additionally wants a signed-in Mi account plus a network connection before it unlocks that one.
The app was not installed
This sentence has one meaning: the phone already holds an app with your package name, and that copy was signed with a different key. Android will not let one app replace another unless the two share a signature, so the installer gives up and says nothing more useful.
Uninstall the old copy, then install. Everything it stored on the phone goes with it, so warn whoever is testing before you do this.
A Development Build is signed with a debug key that lives in your user folder. If that file is gone, Unity quietly makes a new one, and every phone still holding the old build now refuses the new. A second computer, a reinstalled Unity, a colleague's build or a CI server all produce exactly this.
To stop it coming back, sign with a real keystore you keep and share: Project Settings, Player, Publishing Settings, Custom Keystore. One file, one key, every machine — and never commit the keystore or its passwords to git.
adb install Builds/game.apk
# Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.yourstudio.yourgame
# signatures do not match previously installed version; ignoring!] -> a different key
# Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected] -> older versionCode
# Failure [INSTALL_FAILED_OLDER_SDK: ... is greater than the device ...] -> API too low
# Failure [INSTALL_FAILED_NO_MATCHING_ABIS: Failed to extract native libraries] -> wrong ABI
# what the phone actually is, and whether an old copy survived
adb shell getprop ro.build.version.sdk
adb shell getprop ro.product.cpu.abi
adb shell pm list packages | grep yourgame
adb uninstall com.yourstudio.yourgame
adb install Builds/game.apkOver 150 MB: AAB versus APK
Two size limits live in this story and neither one is about your phone, which is why the course ships both kinds of file. Confusing them produces a build that installs perfectly for you and cannot be uploaded, or an upload that no phone can install.
A file you copy onto a phone has no size limit whatsoever. A 400 MB APK installs. The numbers people quote are store numbers.
Google Play caps a single APK at 100 MB and an app bundle's total download size at 150 MB, around 1 GB if you turn on its advanced shrinking. So the file you hand a friend is allowed to be bigger than the file you hand Google.
Tick Build App Bundle in Build Profiles and Unity outputs a .aab that no phone can install — by design, since Google splits it into per-device APKs on its own servers. For a file to test on, untick it and build again.
Big files also fail in transit. An APK pushed through a chat app or a poor cable often lands truncated, and Android calls the result an invalid package. Compare the exact byte count on both machines before you rebuild anything.
Your device isn't compatible with this version
Three settings produce this message, all three of them in Player Settings rather than anywhere on the phone. The useful part is that the phone will print its own number, so this is a comparison and not a guess.
- 1
Minimum API Level above the phone. Run adb shell getprop ro.build.version.sdk and compare: Android 9 is 28, 10 is 29, 12 is 31, 13 is 33, 14 is 34. If the number in Player Settings is the higher of the two, this is your message. 24 reaches effectively every phone still in use.
- 2
Target Architectures ticked wrong. An ARM64-only build onto an older 32-bit phone gives INSTALL_FAILED_NO_MATCHING_ABIS, and an x86 emulator without ARM translation behaves the same way. Tick both ARMv7 and ARM64 unless you have a specific reason not to — the cost is build time, not runtime speed.
- 3
A package name left on com.DefaultCompany.*. Two things follow. On the phone: every project of yours still on the default is one and the same app as far as Android is concerned, so they fight over a single slot and a single signature, which is the previous section arriving early. On the store: that prefix belongs to Unity itself and thousands of people ship with it, so it is not an identity you can defend.
Installed, then closes the moment it opens
Nothing in this group is an install failure — the phone took your app, willingly. The problem is inside the game, and Android hides it behind a silent return to the launcher. One command brings it back out.
# clear last run, then watch only Unity's lines while you reproduce it
adb logcat -c
adb logcat -s Unity
# nothing under the Unity tag? then it is a native crash, not a C# one
adb logcat -d > full-log.txt
grep -iE "fatal|androidruntime|libc |SIGSEGV" full-log.txt
# what Android thinks you actually installed, in one shot
adb shell dumpsys package com.yourstudio.yourgame | grep -E "versionCode|minSdk|targetSdk"A stack trace naming a file and a line is a C# error. NullReferenceException is the usual one, and the line it points at is nearly always a reference you never dragged anything into in the Inspector.
No Unity line at all, just a libc or SIGSEGV block, means the crash is native: usually out of memory on a low-end device, or a plugin that ships no ARM64 binary.
Then come the errors that exist only on the device. Three of them look identical from outside — a NullReferenceException on the very first frame — and have nothing in common underneath.
The scene was never added to the build. Being able to open a scene in the editor means nothing; only what is listed in Build Profiles ships. LoadScene on a scene that did not ship leaves you standing in the intro, dereferencing something that was supposed to be there.
Setup that only runs in the editor. Anything inside a UNITY_EDITOR block compiles to nothing on the device, and so does an init call made only from a script sitting in an Editor folder.
StreamingAssets read with System.IO.File. On a phone that folder lives inside the APK, not on the disk, so File.ReadAllText hands back nothing while working flawlessly on your desktop.
using UnityEngine;
using UnityEngine.Networking;
public class LoadOnDevice : MonoBehaviour
{
// File.ReadAllText works in the editor and returns nothing on Android:
// StreamingAssets sits inside the APK there, not on the filesystem.
IEnumerator Start()
{
string path = Application.streamingAssetsPath + "/levels.json";
using var request = UnityWebRequest.Get(path);
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"Cannot read {path}: {request.error}");
yield break;
}
var data = JsonUtility.FromJson<LevelList>(request.downloadHandler.text);
Debug.Log($"Loaded {data.levels.Length} levels");
}
}The fourth device-only cause is IL2CPP itself. Managed Stripping Level Medium or High deletes types the compiler cannot see anyone calling, which is exactly the situation for anything reached through reflection: a class you hand to JsonUtility.FromJson, something built with Activator.CreateInstance, and every method an Inspector UnityEvent names as a string. Prove it before you fix it — set Managed Stripping Level to Minimal and build again. If the crash disappears, stripping was the culprit; then add a link.xml naming only those types and put the level back, because staying on Minimal hands back every byte and every frame the stripper saved you.
<!-- Anywhere under Assets/ — Unity picks up every link.xml it finds. -->
<linker>
<assembly fullname="Assembly-CSharp">
<!-- keep what only reflection can reach: JSON classes, Activator targets -->
<namespace fullname="Game.Data" preserve="all" />
<type fullname="Game.Skills.Fireball" preserve="all" />
</assembly>
<assembly fullname="Newtonsoft.Json" preserve="all" />
</linker>The build never finished
If no file comes out at all, stop walking through an error list — it is not an install problem. Almost every case is one of the four below, and the Android setup lesson in the course covers the installation itself in order.
Missing modules. Android Build Support alone is not enough: Android SDK & NDK Tools and OpenJDK are its two child entries in Unity Hub, Installs, Add Modules, and they come unticked by default.
Empty paths. Unable to locate Android SDK almost never means the SDK is missing — it means Preferences, External Tools has blank Android paths. Tick the three Use Installed boxes and they point themselves back.
A wall of Gradle red. Scroll up to the first What went wrong line — that is the error, and everything below it is Gradle repeating itself. Two plugins asking for different API levels, or a library included twice, accounts for most of it.
It is not frozen, it is IL2CPP. The first build translates every line of your C# into C++ and compiles it, and ten to twenty minutes is normal. Check the progress bar at the bottom-right of the window before you kill Unity and start again.
The 60-second order
- ✓Does the file really end in .apk, and is its byte count identical to the one on your PC? Chat apps rename and truncate.
- ✓Is Install unknown apps on for the app you tapped it in — not for the file manager you did not use?
- ✓adb shell getprop ro.build.version.sdk — at or above your Minimum API Level?
- ✓adb shell getprop ro.product.cpu.abi — arm64-v8a or armeabi-v7a, and is that one ticked in Target Architectures?
- ✓adb uninstall the package, then adb install, and read the Failure line — it names the cause outright.
- ✓It installs but closes: adb logcat -s Unity first, then rebuild with Managed Stripping Level on Minimal to rule out stripping.
- ✓Still no file at all? Preferences, External Tools: none of the three Android paths may be blank.
Now that it installs and stays open, the thing that goes wrong next is the frame rate rather than the file — and the guide from project to APK covers that flow in order, setup first.
the lesson that builds this
Build, install, debug on device
This post is a standalone recipe. In the course, the same thing is built as part of the one project that runs through all five tabs — Export to Mobile, lesson 05.