---
title: "Android vendor SDK integration"
slug: /workflows/android-vendor-sdk/
markdown: /workflows/android-vendor-sdk.md
section: build-platform
tier: 3
summary: "Gradle templates, AGP, native lib collisions and SDK ports."
sources: ['unity/android-vendor-sdk.md']
canonical: https://unityworkflowlibrary.dev/workflows/android-vendor-sdk/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# Android vendor SDK integration

> 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:

```xml
<!-- 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.
