Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/audience/Assets/Plugins.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions examples/audience/Assets/Plugins/Android.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions examples/audience/Assets/Plugins/Android/mainTemplate.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Custom gradle template for the Audience sample.
//
// Studios who enable AUDIENCE_MOBILE_ATTRIBUTION on Android need
// play-services-ads-identifier so the SDK's GAIDBridge can call
// AdvertisingIdClient.getAdvertisingIdInfo via JNI. Without this
// dependency the class is missing at runtime and `gaid` never lands
// on game_launch (the bridge logs ClassNotFoundException and exits).
//
// To replicate this in your own project:
// 1. Player Settings → Publishing Settings → enable "Custom Main
// Gradle Template". Unity will create this file at
// Assets/Plugins/Android/mainTemplate.gradle.
// 2. Add the play-services-ads-identifier line in the dependencies
// block below.
//
// Studios who do NOT enable AUDIENCE_MOBILE_ATTRIBUTION can omit the
// dependency entirely; the SDK's JNI code is stripped at compile time.

apply plugin: 'com.android.library'
**APPLY_PLUGINS**

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
**DEPS**
// Uncomment to enable GAID collection (requires AUDIENCE_MOBILE_ATTRIBUTION
// scripting define + AudienceConfig.EnableMobileAttribution). Without this
// line the SDK still builds and runs, but `gaid` / `gaidLimitAdTracking`
// never ship and a one-line ClassNotFoundException is logged on Init.
// Skip this line if your studio doesn't need install attribution.
// implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
}

android {
namespace "com.unity3d.player"
ndkPath "**NDKPATH**"

compileSdkVersion **APIVERSION**
buildToolsVersion '**BUILDTOOLS**'

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

defaultConfig {
minSdkVersion **MINSDKVERSION**
targetSdkVersion **TARGETSDKVERSION**
ndk {
abiFilters **ABIFILTERS**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
consumerProguardFiles 'proguard-unity.txt'**USER_PROGUARD**
}

lintOptions {
abortOnError false
}

aaptOptions {
noCompress = **NON_COMPRESSED_ASSETS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}**SIGN**

**PACKAGING_OPTIONS**

buildTypes {
debug {
minifyEnabled **MINIFY_DEBUG**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGN_CONFIG**
jniDebuggable true
}
release {
minifyEnabled **MINIFY_RELEASE**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGN_CONFIG**
}
}**PACKAGING**
}**REPOSITORIES****SOURCE_BUILD_SETUP**
**IL_CPP_BUILD_SETUP**

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions examples/audience/ProjectSettings/ProjectSettings.asset
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ PlayerSettings:
Standalone: 0
iPhone: 0
tvOS: 0
overrideDefaultApplicationIdentifier: 0
overrideDefaultApplicationIdentifier: 1
AndroidBundleVersionCode: 1
AndroidMinSdkVersion: 22
AndroidTargetSdkVersion: 0
Expand Down Expand Up @@ -244,7 +244,7 @@ PlayerSettings:
templateDefaultScene: Assets/Scenes/SampleScene.unity
useCustomMainManifest: 0
useCustomLauncherManifest: 0
useCustomMainGradleTemplate: 0
useCustomMainGradleTemplate: 1
useCustomLauncherGradleManifest: 0
useCustomBaseGradleTemplate: 0
useCustomGradlePropertiesTemplate: 0
Expand Down Expand Up @@ -809,6 +809,7 @@ PlayerSettings:
webGLDecompressionFallback: 0
webGLPowerPreference: 2
scriptingDefineSymbols:
Android: AUDIENCE_MOBILE_ATTRIBUTION
iPhone: AUDIENCE_MOBILE_ATTRIBUTION
additionalCompilerArguments: {}
platformArchitecture: {}
Expand Down
13 changes: 8 additions & 5 deletions src/Packages/Audience/Editor/AndroidManifestPostProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

namespace Immutable.Audience.Editor
{
// Injects android.permission.INTERNET into the generated unityLibrary manifest.
// Injects android.permission.INTERNET into the generated unityLibrary
// manifest. The SDK sends events via System.Net.Http.HttpClient (not
// UnityWebRequest), so Unity does not auto-add INTERNET.
//
// The SDK sends events via System.Net.Http.HttpClient, not UnityWebRequest, so
// Unity does not auto-add INTERNET. This post-processor ensures the permission
// is always present regardless of how the package is installed (file:, git, or
// UPM registry), without requiring the studio to set ForceInternetPermission.
// AD_ID is intentionally NOT injected here. It comes from the
// play-services-ads-identifier AAR's own manifest via AGP merging when
// the studio adds the Maven dependency. Injecting it ourselves would
// declare the permission for studios who never pull in the AAR (and so
// can never collect GAID), creating a Play Store Data Safety mismatch.
internal sealed class AndroidManifestPostProcessor : IPostGenerateGradleAndroidProject
{
private const string InternetPermission = "android.permission.INTERNET";
Expand Down
4 changes: 4 additions & 0 deletions src/Packages/Audience/Runtime/Core/AudiencePaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal static class AudiencePaths
private const string QueueDirName = "queue";
private const string InstallReferrerFileName = "install_referrer";
private const string InstallReferrerSentFileName = "install_referrer_sent";
private const string GAIDFileName = "gaid";

internal static string AudienceDir(string persistentDataPath) =>
Path.Combine(persistentDataPath, RootDirName);
Expand All @@ -28,5 +29,8 @@ internal static string InstallReferrerFile(string persistentDataPath) =>

internal static string InstallReferrerSentFile(string persistentDataPath) =>
Path.Combine(AudienceDir(persistentDataPath), InstallReferrerSentFileName);

internal static string GAIDFile(string persistentDataPath) =>
Path.Combine(AudienceDir(persistentDataPath), GAIDFileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@
# minification regardless of fullMode behaviour.
-keep class com.android.installreferrer.** { *; }
-keep interface com.android.installreferrer.** { *; }

# Keep Google Play Services AdvertisingIdClient symbols (GAID).
#
# AdvertisingIdClient and its inner Info class are reflected via JNI from
# GAIDBridge — they have no managed-side reference for R8 to follow, so
# fullMode in studio projects can drop them. Defensive rules ensure the
# getAdvertisingIdInfo / getId / isLimitAdTrackingEnabled surface survives.
-keep class com.google.android.gms.ads.identifier.** { *; }
-keep interface com.google.android.gms.ads.identifier.** { *; }
23 changes: 23 additions & 0 deletions src/Packages/Audience/Runtime/Unity/AudienceUnityHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ private static void Install()

#if UNITY_ANDROID && !UNITY_EDITOR
ImmutableAudience.MobileInstallReferrerProvider = ProvideInstallReferrer;
#if AUDIENCE_MOBILE_ATTRIBUTION
// Gated on the define so a build that disables GAID at compile
// time can't read a stale cache file left over from a prior
// install where the define was on.
ImmutableAudience.MobileAttributionContextProvider = ProvideAndroidAttributionContext;
Comment thread
cursor[bot] marked this conversation as resolved.
#endif
#endif

UnityLifecycleBridge.EnsureExists();
Expand All @@ -63,5 +69,22 @@ private static void Install()
InstallReferrerBridge.EnsureFetchStarted(path!);
return InstallReferrerBridge.GetCachedInstallReferrer(path!);
}

#if UNITY_ANDROID && !UNITY_EDITOR && AUDIENCE_MOBILE_ATTRIBUTION
// Kicks off a background GAID fetch for the next launch (Google
// requires getAdvertisingIdInfo run off the main thread) and returns
// whatever was cached by the previous launch. First launch returns
// an empty dict; launch #2+ ships gaid + gaidLimitAdTracking.
// Exceptions propagate to ImmutableAudience.Init's
// MobileAttributionContextProviderThrew handler.
private static IReadOnlyDictionary<string, object>? ProvideAndroidAttributionContext()
{
var path = _persistentDataPath;
if (string.IsNullOrEmpty(path)) return AttributionContext.Capture();

GAIDBridge.EnsureFetchStarted(path!);
return AttributionContext.Capture(path);
}
#endif
}
}
62 changes: 49 additions & 13 deletions src/Packages/Audience/Runtime/Unity/Mobile/AttributionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

namespace Immutable.Audience.Unity.Mobile
{
// Builds the iOS attribution snapshot that ships on game_launch when
// EnableMobileAttribution is true. ATT status is always read; IDFA is
// only included when status is authorized (Apple returns the all-zeros
// UUID otherwise, which the native bridge filters to null).
// Builds the platform attribution snapshot that ships on game_launch when
// EnableMobileAttribution is true.
//
// iOS: ATT status is always read; IDFA is only included when status is
// authorized (Apple returns the all-zeros UUID otherwise, which the native
// bridge filters to null).
//
// Android: gaid + gaidLimitAdTracking are read from the GAIDBridge disk
// cache populated by the previous launch's background fetch (Google's
// AdvertisingIdClient is sync + must run off main thread, so first launch
// ships nothing — gaidLimitAdTracking shows up on launch #2 onwards).
internal static class AttributionContext
{
// Maps Apple's ATTrackingManagerAuthorizationStatus to the wire
Expand All @@ -25,17 +32,21 @@ internal static string AttStatusToString(int status)
}
}

// Always returns a non-null dictionary — at minimum
// { attStatus: "notDetermined" }. The provider field type is
// nullable for forward-compat with future implementations that may
// want to opt out, but this implementation never returns null.
internal static IReadOnlyDictionary<string, object> Capture()
// persistentDataPath is required for Android (GAID disk cache); iOS
// ignores it. Returns a possibly-empty dict — never null — so callers
// can merge unconditionally.
internal static IReadOnlyDictionary<string, object> Capture(string? persistentDataPath = null)
{
var props = new Dictionary<string, object>();

#if UNITY_IOS || UNITY_EDITOR
// Compiled in on iOS device builds AND in the editor (any target)
// so AttributionContextTests can drive Capture() via the ATTBridge
// test seams. Excluded on real Android device builds so attStatus
// never ships there. Native ATTBridge calls are themselves gated
// by #if UNITY_IOS, so non-iOS editor targets get the safe stubs.
var status = ATTBridge.GetStatus();
var props = new Dictionary<string, object>
{
["attStatus"] = AttStatusToString(status),
};
props["attStatus"] = AttStatusToString(status);

// Only ship IDFA when the user has authorized tracking. The native
// bridge already returns null for the zero-UUID case, but gating
Expand All @@ -47,8 +58,33 @@ internal static IReadOnlyDictionary<string, object> Capture()
if (!string.IsNullOrEmpty(idfa))
props["idfa"] = idfa!;
}
#endif
Comment thread
cursor[bot] marked this conversation as resolved.

#if UNITY_ANDROID && !UNITY_EDITOR && AUDIENCE_MOBILE_ATTRIBUTION
// Gated on AUDIENCE_MOBILE_ATTRIBUTION so a build that disables
// GAID at compile time can't read a stale cache file written by
// a previous install where the define was on.
if (!string.IsNullOrEmpty(persistentDataPath))
{
var info = GAIDBridge.GetCached(persistentDataPath!);
if (info.HasValue)
EmitGaidProps(info.Value, props);
}
#endif

return props;
}

// Defensive emission gate: even if a stale cache from a pre-fix build
// retained a non-empty GAID under opt-out, this method never ships
// the raw identifier when LimitAdTracking is true. gaidLimitAdTracking
// always ships so the pipeline can distinguish "fetched, opted out"
// from "not fetched yet".
internal static void EmitGaidProps(GAIDInfo info, IDictionary<string, object> props)
{
if (!info.LimitAdTracking && !string.IsNullOrEmpty(info.Gaid))
props["gaid"] = info.Gaid;
props["gaidLimitAdTracking"] = info.LimitAdTracking;
}
}
}
Loading
Loading