Tier 3 · Shipping

Android vendor SDK integration

Gradle templates, AGP, native lib collisions and SDK ports.

Gradle templates, AGP, native lib collisions and SDK ports.

Porting a mobile vendor SDK (ads, tournaments, attribution) onto a modern Unity: Gradle templates, AGP, build hooks, native lib collisions, and stripping the SDK back out for a store build.

  • VENDOR-SDK-GRADLE-UNITY6-PORT — porting a mobile SDK (Skillz, ad/attribution/tournament SDKs) to Unity 6 is THREE separate layers, and fixing only the Gradle templates gets you a build that still fails. Vendors ship 2019-era templates plus editor build hooks; Unity 6.5 bundles AGP 9.0.0. Everything below was measured on Unity 6000.5.0f1 + Skillz 2025.0.19, which went from "cannot build at all" to a green APK.

Layer 1 — the templates. Diff the vendor's copies against <UnityInstall>/Editor/Data/PlaybackEngines/AndroidPlayer/Tools/GradleTemplates/, never against memory. What changed by Unity 6.5: the substitution tokens **MINAPI** and a shared **APIVERSION** for targetSdk became **MINSDK** / **TARGETSDK** (**APIVERSION** survives for compileSdk only); baseProjectTemplate moved from allprojects { buildscript { classpath 'com.android.tools.build:gradle:X' } } to plugins { id 'com.android.application' version '9.0.0' apply false }; namespace is mandatory (AGP 8+); dexOptions was removed in AGP 8.0; compileOptions moved to Java 17; and both main and launcher now expect apply from: '../shared/common.gradle'. An unrecognised token is NOT an error — it is copied through literally into build.gradle and dies as a syntax error, so a stale template reads as a mystery Groovy failure. Confirm which tokens a given editor actually supports by string-scanning UnityEditor.Android.Extensions.dll / Unity.Android.Gradle.dll — they are .NET UTF-16, so strip NULs first (tr -d '\0' < x.dll | grep -aoF '**MINSDK**'); a plain ASCII grep silently finds nothing and looks like proof of absence.

Layer 2 — the editor build hooks, which no template edit can reach. Vendors implement IPostGenerateGradleAndroidProject and write directly into the generated project, downstream of every template. Skillz appends android.enableR8=true, an option removed in AGP 7.0, and AGP hard-fails: "Failed to apply plugin 'com.android.internal.application'. The option 'android.enableR8' is deprecated. It was removed in version 7.0." You usually cannot order a cleanup hook after the vendor's, because vendors habitually declare callbackOrder => int.MaxValue. So the fix is to patch the vendor C# — which an SDK upgrade then silently reverts. Guard it (#if UNITY_2021_2_OR_NEWER return;), leave a greppable marker, and record it in a canonical folder as a re-apply step. Watch for the same hooks appending a second unityStreamingAssets= key: Java properties take the LAST one, so the vendor value silently replaces Unity's substituted noCompress list.

Layer 3 — native library collisions. Any SDK carrying React Native (Skillz, several attribution SDKs) ships its own libc++_shared.so, colliding with Unity's NDK copy at :launcher:mergeDebugNativeLibs ("2 files found with path 'lib/arm64-v8a/libc++_shared.so'"). Fix in the launcher module — that is where merging happens — with packaging { jniLibs { pickFirsts += ['**/libc++_shared.so', '**/libjsc.so', '**/libfbjni.so'] } }. unityLibrary is the first merge input, so pickFirst keeps the copy matching Unity's NDK.

Two traps that waste hours on their own: - useCustomLauncherGradleManifest is the flag for the launcher GRADLE TEMPLATE. Unity's field says "Manifest"; there is no useCustomLauncherGradleTemplate. Grepping ProjectSettings for the name you expect returns nothing and makes an active template look inert — so its dexOptions gets left in place as "dead weight" and keeps failing the build. - settingsTemplate.gradle sets RepositoriesMode.PREFER_SETTINGS, which makes every repository declared in baseProjectTemplate's allprojects { repositories { … } } silently ignored. A vendor that adds its Maven/JitPack repo there resolves nothing. Move repos into dependencyResolutionManagement, and place them outside the // Android Resolver Repos markers or EDM4U's next Force Resolve drops them.

Diagnose from the generated project, never the templates: Library/Bee/Android/Prj/IL2CPP/Gradle/ holds the real gradle.properties (unity.agpVersion, unity.compileSdkVersion, resolved minSdk/targetSdk) and the real build.gradle. Grepping it for a leftover \*\*[A-Z_]+\*\* is a one-command proof that substitution succeeded. And note the officially-supported-Unity-version list is not a verdict: Skillz documents 2021.3/2022.3 LTS only, yet the SDK's C# compiled clean on 6.5 untouched — the incompatibility was entirely in the Android build layer.

  • VENDOR-SDK-STRIP-IS-SIX-LAYERS — shipping a store build of a game that also ships a vendor-SDK build is NOT a scripting define, and the two layers that bite are invisible in the project. A define covers your own call sites; it covers nothing the vendor owns. Measured on a tic-tac-toe game (Unity 6000.5.0f1 + Skillz 2026.1.1), taking a store APK from "cannot build" to play-verified on device. The layers, in the order they bit: 1. Your call sites — the define. 2. The SDK's own C# — vendors routinely ship no asmdef, so all 135 files compile into Assembly-CSharp unconditionally. An asmdef defineConstraints you add is reverted by the next SDK import, so removal is the only durable answer. 3. Its IPostGenerateGradleAndroidProject hooks, gated on nothing but UNITY_ANDROID. Skillz rewrites the launcher activity to com.skillz.activity.UnityGameActivity, a class that exists only inside the .aar. Drop the Gradle dependency while leaving the C# and the store build compiles, signs, installs, then dies at launch with ClassNotFoundException before Unity ever starts — no Unity log, just a black screen. 4. Assets/<Sdk>/Resources/ + the Gradle dependency (the size: 240 MB vs 108 MB here). 5. Assets/StreamingAssets/ — bundles live OUTSIDE the SDK folder, so moving it misses them, and StreamingAssets is force-packed like Resources/. The first "clean" APK still carried assets/skillz-assets + assets/skillz-scenes. Grep the built APK, not the project. 6. The generated Gradle project (Library/Bee/Android/Prj/IL2CPP/Gradle/). It is incremental — Unity never wipes it — and vendor hooks write directly into it, so residue CROSSES build configurations. Skillz's consumer-rules.pro (declaring -dontobfuscate, which AGP 9 rejects in a consumer config) survived the SDK's removal and failed the store build at :unityLibrary:mergeReleaseConsumerProguardFiles. Delete the whole tree when switching configuration; a denylist of the three known filenames goes stale the moment a vendor writes a fourth, and the failure mode is silent cross-config contamination.

The clean mechanism for 2-6 is one action: move the SDK out of Assets/. Unity ignores any directory whose name ends in ~, so a <Sdk>SDK~/ vault is invisible to the importer — no compile, no Resources, no build hooks, on every platform at once. Move it with its .meta files so asset GUIDs survive the round trip and prefab references still bind.

Two traps in implementing the switch itself. (a) Derive current state by reading every layer back off disk, never from an EditorPrefs "flavor" pref — a pref that disagrees with reality is exactly how a store build ships with a vendor launcher activity in it; show the layers separately so a half-applied switch reads as MIXED STATE rather than a single lying boolean. (b) Guard the switch on EditorApplication.isCompiling and Play mode — it moves folders Unity has open — and surface the refusal message, or a chained switch silently no-ops.

  • VENDOR-TEMPLATE-PORT-DIFF-STOCK — when porting a vendor's Gradle templates to a new Unity, diff against the ENGINE's stock template, not just against the build error. A vendor template contains two different things: scaffolding that needs new-AGP syntax fixes, and content the vendor added. Fixing only what the compiler complains about silently keeps the vendor's additions, which then break the moment you try to build without that vendor. Concretely: Unity 6.5 stock declares consumerProguardFiles 'proguard-unity.txt'; Skillz's adds , 'consumer-rules.pro'. Carrying that into a no-vendor template makes Gradle demand a file only the vendor's hook writes, failing with Supplied consumer proguard configuration does not exist — the mirror image of the residue error above, and reached by fixing the first one. Stock templates live at <UnityInstall>/Editor/Data/PlaybackEngines/AndroidPlayer/Tools/GradleTemplates/; diff comment- and blank-stripped, and justify every remaining line (EDM4U's generated repo/exclusion blocks are legitimately shared with other SDKs and stay).

  • UNITY6-GAMEACTIVITY-BREAKS-VENDOR-ACTIVITIES (HARD) — a vendor SDK that supplies its own Android launcher Activity is almost certainly subclassing com.unity3d.player.UnityPlayerActivity, which Unity 6 does not ship by default. The APK builds, installs, and dies instantly. Unity 6's default Application Entry Point is GameActivity, which ships UnityPlayerGameActivity instead of the legacy UnityPlayerActivity. Any vendor activity extending the legacy class then fails to load, and the logcat names the WRONG class first: Unable to instantiate activity ComponentInfo{...com.skillz.activity.UnityGameActivity} -> ClassNotFoundException: com.skillz.activity.UnityGameActivity -> Caused by: ClassNotFoundException: com.unity3d.player.UnityPlayerActivity. Read the NESTED cause: the vendor class is present in the dex, its superclass is not, so "the vendor class is missing" is a red herring that sends you hunting proguard rules and merge order. Fix: PlayerSettings.Android.applicationEntry = AndroidApplicationEntry.Activity (Player Settings -> Android -> Configuration -> Application Entry Point). It is a flags enum, so both can be included, but a vendor that rewrites the launcher activity in the manifest wants the legacy one selected. Keep GameActivity for builds without that SDK — it is the better default (input handling, modern lifecycle), so scope the setting per build configuration rather than flipping it globally. This is invisible to every build-time check — Gradle, manifest merge, and dex all succeed; only launching on a device catches it. Verified on Unity 6000.5.0f1 + Skillz 2026.1.1: with GameActivity the app crashed before Unity started; with the legacy entry point the vendor shell booted and rendered.

ONE-SOURCE-OF-TRUTH-FOR-VENDOR-DEPS — EDM4U's generated list fights a transitive vendor POM

A vendor SDK whose .aar POM already declares its full dependency set gives you two ways to get those dependencies, and running both breaks the build:

  1. implementation("com.vendor:sdk:<version>") { transitive = true } — the POM supplies the set.
  2. EDM4U resolving VendorDependencies.xml into the // Android Resolver Dependencies block.

Do one. Doing both ships libraries under two coordinates — the POM's and the vendor XML's — and the build dies late, at :launcher:checkReleaseDuplicateClasses or the manifest merger, on messages that name a library nobody declared twice:

Duplicate class com.chauthai.swipereveallayout.SwipeRevealLayout found in modules
  SwipeRevealLayout-1.4.0.aar        (com.github.chthai64:SwipeRevealLayout:1.4.0)
  swipe-reveal-layout-1.4.0.aar      (com.chauthai.swipereveallayout:swipe-reveal-layout:1.4.0)

The generated list also imports the vendor's publishing mistakes. One measured case: a gesturehandler POM on the vendor's own nexus declared a transitive on Example:react-native-reanimated:unspecified — a group and version in no repository — which fails resolution before a line is compiled. Pulled transitively from the SDK aar the same artifact resolves fine; only the generated list reproduced the broken coordinate.

Keep the choice enforced, not remembered. EDM4U repopulates the block whenever it resolves, so the decision has to live in settings, not in a comment someone reads later:

<!-- ProjectSettings/GvhProjectSettings.xml -->
<projectSetting name="GooglePlayServices.AutoResolverEnabled"        value="False" />
<projectSetting name="GooglePlayServices.AutoResolveOnBuild"         value="False" />
<projectSetting name="GooglePlayServices.PromptBeforeAutoResolution" value="False" />
<projectSetting name="GooglePlayServices.AutoResolutionDisabledWarning" value="False" />

The last two matter as much as the first: with resolution merely off, EDM4U still raises a modal asking to turn it back on — see EDITOR-MODAL-FROM-INSIDE-A-BUILD-DEADLOCKS in the agent compile page. And when a fresh project inherits templates from a donor, the donor's GvhProjectSettings.xml is part of the template set — copying Assets/Plugins/Android/*.gradle without it leaves the resolver free to undo the very arrangement the templates document.