Skip to content

Commit c0681ff

Browse files
authored
feat: Managed layer initializes native SDK on Android (#1924)
1 parent daae78d commit c0681ff

File tree

13 files changed

+471
-71
lines changed

13 files changed

+471
-71
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
### API Changes
66

7-
- The native layer on iOS no longer self-initializes before the Unity game starts. Instead, it accepts the options at the end of the configure call. To restore the old behavior, users can opt-in to initializing native first via `IosInitializeNativeFirst`. Note that using this option comes with the limitation of baking the options into the generated Xcode project at build-time. ([#1915](https://github.com/getsentry/sentry-unity/pull/1915))
7+
- The native layer on mobile platforms (iOS and Android) no longer self-initializes before the Unity game starts. Previously, the SDK would use the options at build-time and bake them into the native layer. Instead, the SDK will now take the options passed into the `Configure` callback and use those to initialize the native SDKs. This allows users to modify the native SDK's options at runtime programmatically.
8+
The initialization behaviour is controlled by `IosNativeInitializationType` and `AndroidNativeInitializationType` options. These can be set from `Runtime` (default) to `BuildTime` to restore the previous flow and bake the options into the native projects. ([#1915](https://github.com/getsentry/sentry-unity/pull/1915), [#1924](https://github.com/getsentry/sentry-unity/pull/1924))
9+
10+
### Fixes
11+
12+
- On Android, the SDK not longer freezes the game when failing to sync with the native SDK ([#1927](https://github.com/getsentry/sentry-unity/pull/1927))
813

914
### Dependencies
1015

src/Sentry.Unity.Android/IJniExecutor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace Sentry.Unity.Android;
44

55
internal interface IJniExecutor : IDisposable
66
{
7-
public TResult? Run<TResult>(Func<TResult?> jniOperation);
8-
public void Run(Action jniOperation);
7+
public TResult? Run<TResult>(Func<TResult?> jniOperation, TimeSpan? timeout = null);
8+
public void Run(Action jniOperation, TimeSpan? timeout = null);
99
}
Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using Sentry.Extensibility;
45
using UnityEngine;
56

67
namespace Sentry.Unity.Android;
78

89
internal class JniExecutor : IJniExecutor
910
{
11+
// We're capping out at 16ms - 1 frame at 60 frames per second
12+
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(16);
13+
1014
private readonly CancellationTokenSource _shutdownSource;
1115
private readonly AutoResetEvent _taskEvent;
16+
private readonly IDiagnosticLogger? _logger;
17+
1218
private Delegate _currentTask = null!; // The current task will always be set together with the task event
1319

1420
private TaskCompletionSource<object?>? _taskCompletionSource;
1521

1622
private readonly object _lock = new object();
1723

18-
public JniExecutor()
24+
private bool _isDisposed;
25+
private Thread? _workerThread;
26+
27+
public JniExecutor(IDiagnosticLogger? logger)
1928
{
29+
_logger = logger;
2030
_taskEvent = new AutoResetEvent(false);
2131
_shutdownSource = new CancellationTokenSource();
2232

23-
new Thread(DoWork) { IsBackground = true, Name = "SentryJniExecutorThread" }.Start();
33+
_workerThread = new Thread(DoWork) { IsBackground = true, Name = "SentryJniExecutorThread" };
34+
_workerThread.Start();
2435
}
2536

2637
private void DoWork()
@@ -29,7 +40,7 @@ private void DoWork()
2940

3041
var waitHandles = new[] { _taskEvent, _shutdownSource.Token.WaitHandle };
3142

32-
while (true)
43+
while (!_isDisposed)
3344
{
3445
var index = WaitHandle.WaitAny(waitHandles);
3546
if (index > 0)
@@ -50,74 +61,122 @@ private void DoWork()
5061
_taskCompletionSource?.SetResult(null);
5162
break;
5263
}
53-
case Func<bool?> func1:
64+
case Func<bool> func1:
5465
{
5566
var result = func1.Invoke();
5667
_taskCompletionSource?.SetResult(result);
5768
break;
5869
}
59-
case Func<string?> func2:
70+
case Func<bool?> func2:
6071
{
6172
var result = func2.Invoke();
6273
_taskCompletionSource?.SetResult(result);
6374
break;
6475
}
76+
case Func<string?> func3:
77+
{
78+
var result = func3.Invoke();
79+
_taskCompletionSource?.SetResult(result);
80+
break;
81+
}
6582
default:
66-
throw new ArgumentException("Invalid type for _currentTask.");
83+
throw new NotImplementedException($"Task type '{_currentTask?.GetType()}' with value '{_currentTask}' is not implemented in the JniExecutor.");
6784
}
6885
}
6986
catch (Exception e)
7087
{
71-
Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}");
88+
_logger?.LogError(e, "Error during JNI execution.");
89+
_taskCompletionSource?.SetException(e);
7290
}
7391
}
7492

7593
AndroidJNI.DetachCurrentThread();
7694
}
7795

78-
public TResult? Run<TResult>(Func<TResult?> jniOperation)
96+
public TResult? Run<TResult>(Func<TResult?> jniOperation, TimeSpan? timeout = null)
7997
{
8098
lock (_lock)
8199
{
100+
timeout ??= DefaultTimeout;
101+
using var timeoutCts = new CancellationTokenSource(timeout.Value);
82102
_taskCompletionSource = new TaskCompletionSource<object?>();
83103
_currentTask = jniOperation;
84104
_taskEvent.Set();
85105

86106
try
87107
{
88-
return (TResult?)_taskCompletionSource.Task.GetAwaiter().GetResult();
108+
_taskCompletionSource.Task.Wait(timeoutCts.Token);
109+
return (TResult?)_taskCompletionSource.Task.Result;
110+
}
111+
catch (OperationCanceledException)
112+
{
113+
_logger?.LogError("JNI execution timed out.");
114+
return default;
89115
}
90116
catch (Exception e)
91117
{
92-
Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}");
118+
_logger?.LogError(e, "Error during JNI execution.");
119+
return default;
120+
}
121+
finally
122+
{
123+
_currentTask = null!;
93124
}
94-
95-
return default;
96125
}
97126
}
98127

99-
public void Run(Action jniOperation)
128+
public void Run(Action jniOperation, TimeSpan? timeout = null)
100129
{
101130
lock (_lock)
102131
{
132+
timeout ??= DefaultTimeout;
133+
using var timeoutCts = new CancellationTokenSource(timeout.Value);
103134
_taskCompletionSource = new TaskCompletionSource<object?>();
104135
_currentTask = jniOperation;
105136
_taskEvent.Set();
106137

107138
try
108139
{
109-
_taskCompletionSource.Task.Wait();
140+
_taskCompletionSource.Task.Wait(timeoutCts.Token);
141+
}
142+
catch (OperationCanceledException)
143+
{
144+
_logger?.LogError("JNI execution timed out.");
110145
}
111146
catch (Exception e)
112147
{
113-
Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}");
148+
_logger?.LogError(e, "Error during JNI execution.");
149+
}
150+
finally
151+
{
152+
_currentTask = null!;
114153
}
115154
}
116155
}
117156

118157
public void Dispose()
119158
{
159+
if (_isDisposed)
160+
{
161+
return;
162+
}
163+
164+
_isDisposed = true;
165+
120166
_shutdownSource.Cancel();
167+
try
168+
{
169+
_workerThread?.Join(100);
170+
}
171+
catch (ThreadStateException)
172+
{
173+
_logger?.LogError("JNI Executor Worker thread was never started during disposal");
174+
}
175+
catch (ThreadInterruptedException)
176+
{
177+
_logger?.LogError("JNI Executor Worker thread was interrupted during disposal");
178+
}
179+
121180
_taskEvent.Dispose();
122181
}
123182
}

src/Sentry.Unity.Android/SentryJava.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
using System;
2+
using System.Diagnostics;
3+
using Sentry.Extensibility;
24
using UnityEngine;
5+
using Debug = UnityEngine.Debug;
36

47
namespace Sentry.Unity.Android;
58

69
internal interface ISentryJava
710
{
11+
public bool IsEnabled(IJniExecutor jniExecutor);
12+
public bool Init(IJniExecutor jniExecutor, SentryUnityOptions options, TimeSpan timeout);
813
public string? GetInstallationId(IJniExecutor jniExecutor);
914
public bool? CrashedLastRun(IJniExecutor jniExecutor);
1015
public void Close(IJniExecutor jniExecutor);
@@ -40,6 +45,93 @@ internal class SentryJava : ISentryJava
4045
{
4146
private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry");
4247

48+
public bool IsEnabled(IJniExecutor jniExecutor)
49+
{
50+
return jniExecutor.Run(() =>
51+
{
52+
using var sentry = GetSentryJava();
53+
return sentry.CallStatic<bool>("isEnabled");
54+
});
55+
}
56+
57+
public bool Init(IJniExecutor jniExecutor, SentryUnityOptions options, TimeSpan timeout)
58+
{
59+
jniExecutor.Run(() =>
60+
{
61+
using var sentry = new AndroidJavaClass("io.sentry.android.core.SentryAndroid");
62+
using var context = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
63+
.GetStatic<AndroidJavaObject>("currentActivity");
64+
65+
sentry.CallStatic("init", context, new AndroidOptionsConfiguration(androidOptions =>
66+
{
67+
androidOptions.Call("setDsn", options.Dsn);
68+
androidOptions.Call("setDebug", options.Debug);
69+
androidOptions.Call("setRelease", options.Release);
70+
androidOptions.Call("setEnvironment", options.Environment);
71+
72+
var sentryLevelClass = new AndroidJavaClass("io.sentry.SentryLevel");
73+
var levelString = GetLevelString(options.DiagnosticLevel);
74+
var sentryLevel = sentryLevelClass.GetStatic<AndroidJavaObject>(levelString);
75+
androidOptions.Call("setDiagnosticLevel", sentryLevel);
76+
77+
if (options.SampleRate.HasValue)
78+
{
79+
androidOptions.SetIfNotNull("setSampleRate", options.SampleRate.Value);
80+
}
81+
82+
androidOptions.Call("setMaxBreadcrumbs", options.MaxBreadcrumbs);
83+
androidOptions.Call("setMaxCacheItems", options.MaxCacheItems);
84+
androidOptions.Call("setSendDefaultPii", options.SendDefaultPii);
85+
androidOptions.Call("setEnableNdk", options.NdkIntegrationEnabled);
86+
androidOptions.Call("setEnableScopeSync", options.NdkScopeSyncEnabled);
87+
88+
// Options that are not to be set by the user
89+
// We're disabling some integrations as to not duplicate event or because the SDK relies on the .NET SDK
90+
// implementation of certain feature - i.e. Session Tracking
91+
92+
// Note: doesn't work - produces a blank (white) screenshot
93+
androidOptions.Call("setAttachScreenshot", false);
94+
androidOptions.Call("setEnableAutoSessionTracking", false);
95+
androidOptions.Call("setEnableActivityLifecycleBreadcrumbs", false);
96+
androidOptions.Call("setAnrEnabled", false);
97+
androidOptions.Call("setEnableScopePersistence", false);
98+
}, options.DiagnosticLogger));
99+
}, timeout);
100+
101+
return IsEnabled(jniExecutor);
102+
}
103+
104+
internal class AndroidOptionsConfiguration : AndroidJavaProxy
105+
{
106+
private readonly Action<AndroidJavaObject> _callback;
107+
private readonly IDiagnosticLogger? _logger;
108+
109+
public AndroidOptionsConfiguration(Action<AndroidJavaObject> callback, IDiagnosticLogger? logger)
110+
: base("io.sentry.Sentry$OptionsConfiguration")
111+
{
112+
_callback = callback;
113+
_logger = logger;
114+
}
115+
116+
public override AndroidJavaObject? Invoke(string methodName, AndroidJavaObject[] args)
117+
{
118+
try
119+
{
120+
if (methodName != "configure" || args.Length != 1)
121+
{
122+
throw new Exception($"Invalid invocation: {methodName}({args.Length} args)");
123+
}
124+
125+
_callback(args[0]);
126+
}
127+
catch (Exception e)
128+
{
129+
_logger?.LogError(e, "Error invoking {0} ’.", methodName);
130+
}
131+
return null;
132+
}
133+
}
134+
43135
public string? GetInstallationId(IJniExecutor jniExecutor)
44136
{
45137
return jniExecutor.Run(() =>
@@ -165,6 +257,17 @@ public ScopeCallback(Action<AndroidJavaObject> callback) : base("io.sentry.Scope
165257
return null;
166258
}
167259
}
260+
261+
// https://github.com/getsentry/sentry-java/blob/db4dfc92f202b1cefc48d019fdabe24d487db923/sentry/src/main/java/io/sentry/SentryLevel.java#L4-L9
262+
internal static string GetLevelString(SentryLevel level) => level switch
263+
{
264+
SentryLevel.Debug => "DEBUG",
265+
SentryLevel.Error => "ERROR",
266+
SentryLevel.Fatal => "FATAL",
267+
SentryLevel.Info => "INFO",
268+
SentryLevel.Warning => "WARNING",
269+
_ => "DEBUG"
270+
};
168271
}
169272

170273
internal static class AndroidJavaObjectExtension
@@ -186,6 +289,8 @@ public static void SetIfNotNull<T>(this AndroidJavaObject javaObject, string pro
186289
}
187290
public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, int? value) =>
188291
SetIfNotNull(javaObject, property, value, "java.lang.Integer");
292+
public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, bool value) =>
293+
SetIfNotNull(javaObject, property, value, "java.lang.Boolean");
189294
public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, bool? value) =>
190295
SetIfNotNull(javaObject, property, value, "java.lang.Boolean");
191296
}

src/Sentry.Unity.Android/SentryNativeAndroid.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,18 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry
3737
return;
3838
}
3939

40-
JniExecutor ??= new JniExecutor();
40+
JniExecutor ??= new JniExecutor(options.DiagnosticLogger);
41+
42+
if (SentryJava.IsEnabled(JniExecutor))
43+
{
44+
options.DiagnosticLogger?.LogDebug("The Android SDK is already initialized");
45+
}
46+
// Local testing had Init at an average of about 25ms.
47+
else if (!SentryJava.Init(JniExecutor, options, TimeSpan.FromMilliseconds(200)))
48+
{
49+
options.DiagnosticLogger?.LogError("Failed to initialize Android Native Support");
50+
return;
51+
}
4152

4253
options.NativeContextWriter = new NativeContextWriter(JniExecutor, SentryJava);
4354
options.ScopeObserver = new AndroidJavaScopeObserver(options, JniExecutor);
@@ -98,6 +109,8 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry
98109
options.DiagnosticLogger?.LogDebug("Failed to create new 'Default User ID'.");
99110
}
100111
}
112+
113+
options.DiagnosticLogger?.LogInfo("Successfully configured the Android SDK");
101114
}
102115

103116
/// <summary>
@@ -119,7 +132,7 @@ internal static void Close(SentryUnityOptions options, ISentryUnityInfo sentryUn
119132

120133
// This is an edge-case where the Android SDK has been enabled and setup during build-time but is being
121134
// shut down at runtime. In this case Configure() has not been called and there is no JniExecutor yet
122-
JniExecutor ??= new JniExecutor();
135+
JniExecutor ??= new JniExecutor(options.DiagnosticLogger);
123136
SentryJava?.Close(JniExecutor);
124137
JniExecutor.Dispose();
125138
}

0 commit comments

Comments
 (0)