diff --git a/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs
new file mode 100644
index 00000000..c17aa1df
--- /dev/null
+++ b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs
@@ -0,0 +1,36 @@
+#nullable enable
+
+using UnityEditor;
+
+namespace Immutable.Audience.Editor
+{
+ ///
+ /// Adds the mobile attribution toggles to .
+ ///
+ ///
+ /// The toggles read and write directly.
+ /// They are not fields on the asset, so an existing define set manually
+ /// (the old way) shows correctly the first time this asset is opened,
+ /// and nothing gets reset by updating the package.
+ ///
+ [CustomEditor(typeof(AudienceMobileBuildSettings))]
+ internal sealed class AudienceMobileBuildSettingsEditor : UnityEditor.Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ EditorGUILayout.LabelField("Mobile Attribution", EditorStyles.boldLabel);
+ DrawToggle(BuildTargetGroup.iOS, "Enable for iOS");
+ DrawToggle(BuildTargetGroup.Android, "Enable for Android");
+ EditorGUILayout.Space();
+
+ DrawDefaultInspector();
+ }
+
+ private static void DrawToggle(BuildTargetGroup group, string label)
+ {
+ var enabled = MobileAttributionDefine.IsEnabled(group);
+ var toggled = EditorGUILayout.Toggle(label, enabled);
+ if (toggled != enabled) MobileAttributionDefine.SetEnabled(group, toggled);
+ }
+ }
+}
diff --git a/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta
new file mode 100644
index 00000000..4fbdee1c
--- /dev/null
+++ b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2fdd843fb8feb4b1e9cc8c97d88748c7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/Packages/Audience/Editor/MobileAttributionDefine.cs b/src/Packages/Audience/Editor/MobileAttributionDefine.cs
new file mode 100644
index 00000000..8843abbc
--- /dev/null
+++ b/src/Packages/Audience/Editor/MobileAttributionDefine.cs
@@ -0,0 +1,52 @@
+#nullable enable
+
+using System.Collections.Generic;
+using UnityEditor;
+
+namespace Immutable.Audience.Editor
+{
+ ///
+ /// Reads and writes the AUDIENCE_MOBILE_ATTRIBUTION scripting
+ /// define on Player Settings.
+ ///
+ ///
+ /// No state is cached or serialized here. Player Settings is the only
+ /// source of truth, so this always reflects whatever a studio has
+ /// already set, including manually, and nothing here can go stale.
+ ///
+ internal static class MobileAttributionDefine
+ {
+ internal const string Symbol = "AUDIENCE_MOBILE_ATTRIBUTION";
+
+ internal static bool IsEnabled(BuildTargetGroup group) =>
+ Contains(PlayerSettings.GetScriptingDefineSymbolsForGroup(group));
+
+ internal static void SetEnabled(BuildTargetGroup group, bool enabled)
+ {
+ var current = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
+ PlayerSettings.SetScriptingDefineSymbolsForGroup(group, WithSymbol(current, enabled));
+ }
+
+ internal static bool Contains(string? defines)
+ {
+ foreach (var define in (defines ?? string.Empty).Split(';'))
+ {
+ if (define.Trim() == Symbol) return true;
+ }
+ return false;
+ }
+
+ internal static string WithSymbol(string? defines, bool enabled)
+ {
+ var result = new List();
+ foreach (var define in (defines ?? string.Empty).Split(';', System.StringSplitOptions.RemoveEmptyEntries))
+ {
+ var trimmed = define.Trim();
+ if (trimmed.Length == 0 || trimmed == Symbol) continue;
+ result.Add(trimmed);
+ }
+ if (enabled) result.Add(Symbol);
+ return string.Join(";", result);
+ }
+ }
+}
diff --git a/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta b/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta
new file mode 100644
index 00000000..bb0f58b6
--- /dev/null
+++ b/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 225fcb5f29be241f4a71fb94096a09a1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs b/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs
index c4c1f4b7..3fdb1808 100644
--- a/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs
+++ b/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs
@@ -50,7 +50,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr
if (target != BuildTarget.iOS) return;
#if UNITY_IOS
- if (!AttributionDefineEnabled()) return;
+ if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return;
var pbxPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
if (!File.Exists(pbxPath))
@@ -75,18 +75,5 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr
pbx.WriteToFile(pbxPath);
#endif
}
-
- // Reads the iOS-target define list specifically. The post-processor
- // mutates iOS build output regardless of which target the editor is
- // currently focused on.
- private static bool AttributionDefineEnabled()
- {
- var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty;
- foreach (var define in defines.Split(';'))
- {
- if (define.Trim() == iOSInfoPlistPostProcessor.AttributionDefine) return true;
- }
- return false;
- }
}
}
diff --git a/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs b/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs
index 4061373a..92718fb7 100644
--- a/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs
+++ b/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs
@@ -34,7 +34,6 @@ namespace Immutable.Audience.Editor
internal static class iOSInfoPlistPostProcessor
{
internal const int CallbackOrder = 9050;
- internal const string AttributionDefine = "AUDIENCE_MOBILE_ATTRIBUTION";
[PostProcessBuild(CallbackOrder)]
internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
@@ -42,7 +41,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr
if (target != BuildTarget.iOS) return;
#if UNITY_IOS
- if (!AttributionDefineEnabled()) return;
+ if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return;
var plistPath = Path.Combine(pathToBuiltProject, "Info.plist");
if (!File.Exists(plistPath))
@@ -68,12 +67,11 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr
[MenuItem("Tools/Immutable/Audience/Validate iOS Build Settings")]
private static void ValidateBuildSettings()
{
- if (!AttributionDefineEnabled())
+ if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS))
{
Debug.LogWarning(
- "[ImmutableAudience] AUDIENCE_MOBILE_ATTRIBUTION scripting define is not set " +
- "for the iOS player target. The post-processor will not modify Info.plist. " +
- "Add the define under Player Settings → Other Settings → Scripting Define Symbols.");
+ "[ImmutableAudience] Mobile attribution is not enabled for iOS. The post-processor " +
+ "will not modify Info.plist. Enable it on the AudienceMobileBuildSettings asset.");
return;
}
@@ -92,19 +90,6 @@ private static void ValidateBuildSettings()
: string.Concat(System.Array.ConvertAll(ids, id => $" - {id}\n"))));
}
- // Reads the iOS-target define list specifically. The post-processor
- // mutates iOS build output regardless of which target the editor is
- // currently focused on.
- private static bool AttributionDefineEnabled()
- {
- var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty;
- foreach (var define in defines.Split(';'))
- {
- if (define.Trim() == AttributionDefine) return true;
- }
- return false;
- }
-
#if UNITY_IOS
internal static void ApplyTrackingUsageDescription(
PlistElementDict root,
diff --git a/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs b/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs
index 16542cf8..96b89ec9 100644
--- a/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs
+++ b/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs
@@ -29,7 +29,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr
if (target != BuildTarget.iOS) return;
#if UNITY_IOS
- if (!AttributionDefineEnabled()) return;
+ if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return;
var builtManifestPath = FindBuiltManifest(pathToBuiltProject);
if (builtManifestPath == null)
@@ -119,15 +119,5 @@ v is PlistElementString s &&
purposes.AddString("NSPrivacyCollectedDataTypePurposeAnalytics");
}
#endif
-
- private static bool AttributionDefineEnabled()
- {
- var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty;
- foreach (var define in defines.Split(';'))
- {
- if (define.Trim() == iOSInfoPlistPostProcessor.AttributionDefine) return true;
- }
- return false;
- }
}
}
diff --git a/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs
new file mode 100644
index 00000000..ce737056
--- /dev/null
+++ b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs
@@ -0,0 +1,79 @@
+#nullable enable
+
+using NUnit.Framework;
+using Immutable.Audience.Editor;
+
+namespace Immutable.Audience.Editor.Tests
+{
+ [TestFixture]
+ internal class MobileAttributionDefineTests
+ {
+ [Test]
+ public void Contains_NullOrEmpty_ReturnsFalse()
+ {
+ Assert.IsFalse(MobileAttributionDefine.Contains(null));
+ Assert.IsFalse(MobileAttributionDefine.Contains(""));
+ }
+
+ [Test]
+ public void Contains_SymbolAmongOthers_ReturnsTrue()
+ {
+ Assert.IsTrue(MobileAttributionDefine.Contains("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR"));
+ }
+
+ [Test]
+ public void Contains_SymbolWithStrayWhitespace_ReturnsTrue()
+ {
+ Assert.IsTrue(MobileAttributionDefine.Contains("FOO; AUDIENCE_MOBILE_ATTRIBUTION ;BAR"));
+ }
+
+ [Test]
+ public void Contains_OtherDefinesOnly_ReturnsFalse()
+ {
+ Assert.IsFalse(MobileAttributionDefine.Contains("FOO;BAR"));
+ }
+
+ [Test]
+ public void WithSymbol_EnableOnEmpty_AddsOnlySymbol()
+ {
+ Assert.AreEqual("AUDIENCE_MOBILE_ATTRIBUTION", MobileAttributionDefine.WithSymbol(null, enabled: true));
+ }
+
+ [Test]
+ public void WithSymbol_EnableWithOthers_KeepsOthersAndAddsSymbol()
+ {
+ var result = MobileAttributionDefine.WithSymbol("FOO;BAR", enabled: true);
+
+ Assert.IsTrue(MobileAttributionDefine.Contains(result));
+ Assert.IsTrue(result.Contains("FOO"));
+ Assert.IsTrue(result.Contains("BAR"));
+ }
+
+ [Test]
+ public void WithSymbol_EnableWhenAlreadyPresent_DoesNotDuplicate()
+ {
+ var result = MobileAttributionDefine.WithSymbol("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR", enabled: true);
+
+ var symbolCount = System.Array.FindAll(result.Split(';'), d => d == "AUDIENCE_MOBILE_ATTRIBUTION").Length;
+ Assert.AreEqual(1, symbolCount);
+ }
+
+ [Test]
+ public void WithSymbol_DisableRemovesSymbolOnly()
+ {
+ var result = MobileAttributionDefine.WithSymbol("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR", enabled: false);
+
+ Assert.IsFalse(MobileAttributionDefine.Contains(result));
+ Assert.IsTrue(result.Contains("FOO"));
+ Assert.IsTrue(result.Contains("BAR"));
+ }
+
+ [Test]
+ public void WithSymbol_DisableWhenAbsent_LeavesOthersUnchanged()
+ {
+ var result = MobileAttributionDefine.WithSymbol("FOO;BAR", enabled: false);
+
+ Assert.AreEqual("FOO;BAR", result);
+ }
+ }
+}
diff --git a/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta
new file mode 100644
index 00000000..7b3edd11
--- /dev/null
+++ b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0b4d32623b1c9476a9a1775f96542ab2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: