Skip to content

Commit 76b5c8d

Browse files
authored
Merge pull request #739 from immutable/feat/sdk-297-298-idfv-device-signals
feat(audience-sdk): iOS IDFV bridge and device signals (SDK-297, SDK-298)
2 parents c3a7356 + 20930d1 commit 76b5c8d

11 files changed

Lines changed: 198 additions & 3 deletions

File tree

src/Packages/Audience/Runtime/Plugins.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Audience/Runtime/Plugins/iOS.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#import <Foundation/Foundation.h>
2+
#import <UIKit/UIKit.h>
3+
4+
extern "C" {
5+
6+
const char* _AudienceGetIDFV(void)
7+
{
8+
NSString *idfv = [[UIDevice currentDevice].identifierForVendor UUIDString];
9+
// strdup: IL2CPP calls free() on the returned pointer after copying into a
10+
// managed string. UTF8String is autoreleased (not malloc'd), so free() would
11+
// crash — strdup gives IL2CPP a heap-allocated copy it can safely free.
12+
return idfv ? strdup([idfv UTF8String]) : NULL;
13+
}
14+
15+
}

src/Packages/Audience/Runtime/Plugins/iOS/AudienceMobileBridge.mm.meta

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Audience/Runtime/Unity/DeviceCollector.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Globalization;
6+
using Immutable.Audience.Unity.Mobile;
67
using UnityEngine;
78

89
namespace Immutable.Audience.Unity
@@ -45,11 +46,13 @@ internal static Dictionary<string, object> CollectContext()
4546
return $"{width}x{height}";
4647
}
4748

48-
internal static Dictionary<string, object> CollectGameLaunchProperties()
49+
internal static Dictionary<string, object> CollectGameLaunchProperties(
50+
RuntimePlatform? platformOverride = null)
4951
{
52+
var platform = platformOverride ?? Application.platform;
5053
var props = new Dictionary<string, object>
5154
{
52-
["platform"] = Application.platform.ToString(),
55+
["platform"] = PlatformName(platform),
5356
["version"] = Truncate(Application.version, 256),
5457
["buildGuid"] = Truncate(Application.buildGUID, 256),
5558
["unityVersion"] = Truncate(Application.unityVersion, 256),
@@ -66,9 +69,18 @@ internal static Dictionary<string, object> CollectGameLaunchProperties()
6669
var dpi = (int)Screen.dpi;
6770
if (dpi > 0) props["screenDpi"] = dpi;
6871

69-
if (Application.platform == RuntimePlatform.Android)
72+
if (platform == RuntimePlatform.Android)
7073
props["androidId"] = Truncate(SystemInfo.deviceUniqueIdentifier, 256);
7174

75+
if (platform == RuntimePlatform.IPhonePlayer)
76+
{
77+
var idfv = IDFVBridge.GetIDFV();
78+
if (idfv != null) props["idfv"] = Truncate(idfv, 256);
79+
80+
// iOS baseline is 163 DPI (1×); 326 → 2×, 401-460 → 3×.
81+
if (dpi > 0) props["screenScale"] = (int)Math.Round(dpi / 163.0);
82+
}
83+
7284
return props;
7385
}
7486

@@ -92,6 +104,12 @@ internal static Dictionary<string, object> CollectGameLaunchProperties()
92104
}
93105
}
94106

107+
private static string PlatformName(RuntimePlatform platform) => platform switch
108+
{
109+
RuntimePlatform.IPhonePlayer => "iOS",
110+
_ => platform.ToString(),
111+
};
112+
95113
private static string Truncate(string s, int max)
96114
{
97115
if (string.IsNullOrEmpty(s) || s.Length <= max) return s;

src/Packages/Audience/Runtime/Unity/Mobile.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#nullable enable
2+
3+
using System;
4+
#if UNITY_IOS
5+
using System.Runtime.InteropServices;
6+
#endif
7+
8+
namespace Immutable.Audience.Unity.Mobile
9+
{
10+
internal static class IDFVBridge
11+
{
12+
// Replaceable in tests — captures NativeImpl by default.
13+
internal static Func<string?> Impl = NativeImpl;
14+
15+
internal static string? GetIDFV() => Impl();
16+
17+
#if UNITY_IOS
18+
[DllImport("__Internal")]
19+
private static extern string _AudienceGetIDFV();
20+
21+
private static string? NativeImpl() => _AudienceGetIDFV();
22+
#else
23+
private static string? NativeImpl() => null;
24+
#endif
25+
}
26+
}

src/Packages/Audience/Runtime/Unity/Mobile/IDFVBridge.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Audience/Tests/Runtime/Unity.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Audience/Tests/Runtime/Unity/DeviceCollectorTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#nullable enable
22

3+
using System;
34
using System.Collections.Generic;
45
using NUnit.Framework;
56
using Immutable.Audience.Unity;
7+
using Immutable.Audience.Unity.Mobile;
8+
using UnityEngine;
69

710
namespace Immutable.Audience.Tests
811
{
@@ -89,5 +92,52 @@ public void CollectGameLaunchProperties_ScreenDpi_AbsentWhenZero()
8992
if (props.TryGetValue("screenDpi", out var dpi))
9093
Assert.Greater((int)dpi, 0, "screenDpi must not be 0 when present");
9194
}
95+
96+
// -----------------------------------------------------------------
97+
// iOS-specific (IDFVBridge + screenScale)
98+
// -----------------------------------------------------------------
99+
100+
private Func<string?>? _originalIDFVImpl;
101+
102+
[SetUp]
103+
public void SetUp() => _originalIDFVImpl = IDFVBridge.Impl;
104+
105+
[TearDown]
106+
public void TearDown() => IDFVBridge.Impl = _originalIDFVImpl!;
107+
108+
[Test]
109+
public void CollectGameLaunchProperties_iOS_IdfvPresentWhenBridgeReturnsValue()
110+
{
111+
IDFVBridge.Impl = () => "12345678-ABCD-ABCD-ABCD-123456789ABC";
112+
var props = DeviceCollector.CollectGameLaunchProperties(RuntimePlatform.IPhonePlayer);
113+
Assert.IsTrue(props.ContainsKey("idfv"), "idfv must be present when bridge returns a value");
114+
Assert.AreEqual("12345678-ABCD-ABCD-ABCD-123456789ABC", props["idfv"]);
115+
}
116+
117+
[Test]
118+
public void CollectGameLaunchProperties_iOS_IdfvAbsentWhenBridgeReturnsNull()
119+
{
120+
IDFVBridge.Impl = () => null;
121+
var props = DeviceCollector.CollectGameLaunchProperties(RuntimePlatform.IPhonePlayer);
122+
Assert.IsFalse(props.ContainsKey("idfv"), "idfv must be absent when bridge returns null");
123+
}
124+
125+
[Test]
126+
public void CollectGameLaunchProperties_NonIOS_DoesNotContainIdfv()
127+
{
128+
// IDFVBridge must never be called on non-iOS platforms.
129+
IDFVBridge.Impl = () => "should-not-appear";
130+
var props = DeviceCollector.CollectGameLaunchProperties();
131+
Assert.IsFalse(props.ContainsKey("idfv"),
132+
"idfv must not be present on non-iOS platforms");
133+
}
134+
135+
[Test]
136+
public void CollectGameLaunchProperties_iOS_ContainsPlatformIPhonePlayer()
137+
{
138+
IDFVBridge.Impl = () => null;
139+
var props = DeviceCollector.CollectGameLaunchProperties(RuntimePlatform.IPhonePlayer);
140+
Assert.AreEqual("iOS", props["platform"]);
141+
}
92142
}
93143
}

0 commit comments

Comments
 (0)