Skip to content

Commit cf7680d

Browse files
authored
Enable deferred installation of Crashlytics NDK signal handler. (#2986)
Some app development platforms, including Unity, will overwrite our NDK signal handler if it is installed too quickly upon app launch. This change prevents our signal handler from being immediately installed, instead waiting for FirebaseCrashlyticsNdk#installSignalHander(). Unity-specific logic was added to CrashlyticsNdkRegistrar.
1 parent 9b67604 commit cf7680d

File tree

9 files changed

+117
-42
lines changed

9 files changed

+117
-42
lines changed

firebase-crashlytics-ndk/src/androidTest/java/com/google/firebase/crashlytics/ndk/FirebaseCrashlyticsNdkTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class FirebaseCrashlyticsNdkTest extends TestCase {
2929
protected void setUp() throws Exception {
3030
super.setUp();
3131
mockController = mock(CrashpadController.class);
32-
nativeComponent = new FirebaseCrashlyticsNdk(mockController);
32+
nativeComponent = new FirebaseCrashlyticsNdk(mockController, true);
3333
}
3434

3535
@Override

firebase-crashlytics-ndk/src/main/java/com/google/firebase/crashlytics/ndk/CrashlyticsNdkRegistrar.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.firebase.components.ComponentRegistrar;
2121
import com.google.firebase.components.Dependency;
2222
import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent;
23+
import com.google.firebase.crashlytics.internal.unity.ResourceUnityVersionProvider;
2324
import com.google.firebase.platforminfo.LibraryVersionComponent;
2425
import java.util.Arrays;
2526
import java.util.List;
@@ -38,6 +39,10 @@ public List<Component<?>> getComponents() {
3839

3940
private CrashlyticsNativeComponent buildCrashlyticsNdk(ComponentContainer container) {
4041
Context context = container.get(Context.class);
41-
return FirebaseCrashlyticsNdk.create(context);
42+
// The signal handler is installed immediately for non-Unity apps. For Unity apps, it will
43+
// be installed when the Firebase Unity SDK explicitly calls installSignalHandler().
44+
boolean installHandlerDuringPrepSession =
45+
(ResourceUnityVersionProvider.resolveUnityEditorVersion(context) == null);
46+
return FirebaseCrashlyticsNdk.create(context, installHandlerDuringPrepSession);
4247
}
4348
}

firebase-crashlytics-ndk/src/main/java/com/google/firebase/crashlytics/ndk/FirebaseCrashlyticsNdk.java

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,62 @@ class FirebaseCrashlyticsNdk implements CrashlyticsNativeComponent {
2828
/** Relative sub-path to use for storing files. */
2929
private static final String FILES_PATH = ".com.google.firebase.crashlytics-ndk";
3030

31-
static FirebaseCrashlyticsNdk create(@NonNull Context context) {
31+
private static FirebaseCrashlyticsNdk instance;
32+
33+
static FirebaseCrashlyticsNdk create(
34+
@NonNull Context context, boolean installHandlerDuringPrepareSession) {
3235
final File rootDir = new File(context.getFilesDir(), FILES_PATH);
3336

3437
final CrashpadController controller =
3538
new CrashpadController(
3639
context, new JniNativeApi(context), new NdkCrashFilesManager(rootDir));
37-
return new FirebaseCrashlyticsNdk(controller);
40+
41+
instance = new FirebaseCrashlyticsNdk(controller, installHandlerDuringPrepareSession);
42+
return instance;
3843
}
3944

4045
private final CrashpadController controller;
4146

42-
FirebaseCrashlyticsNdk(@NonNull CrashpadController controller) {
47+
private interface SignalHandlerInstaller {
48+
void installHandler();
49+
}
50+
51+
private boolean installHandlerDuringPrepareSession;
52+
private SignalHandlerInstaller signalHandlerInstaller;
53+
54+
FirebaseCrashlyticsNdk(
55+
@NonNull CrashpadController controller, boolean installHandlerDuringPrepareSession) {
4356
this.controller = controller;
57+
this.installHandlerDuringPrepareSession = installHandlerDuringPrepareSession;
4458
}
4559

4660
@Override
4761
public boolean hasCrashDataForSession(@NonNull String sessionId) {
4862
return controller.hasCrashDataForSession(sessionId);
4963
}
5064

65+
/**
66+
* Prepares the session to be opened. If installSignalHandlerDuringPrepareSession was false at the
67+
* constructor, the signal handler will not be fully installed until {@link
68+
* FirebaseCrashlyticsNdk#installSignalHandler()} is called.
69+
*/
5170
@Override
52-
public void openSession(
71+
public synchronized void prepareNativeSession(
5372
@NonNull String sessionId,
5473
@NonNull String generator,
5574
long startedAtSeconds,
5675
@NonNull StaticSessionData sessionData) {
5776

58-
Logger.getLogger().d("Opening native session: " + sessionId);
59-
if (!controller.initialize(sessionId, generator, startedAtSeconds, sessionData)) {
60-
Logger.getLogger().w("Failed to initialize Crashlytics NDK for session " + sessionId);
77+
signalHandlerInstaller =
78+
() -> {
79+
Logger.getLogger().d("Initializing native session: " + sessionId);
80+
if (!controller.initialize(sessionId, generator, startedAtSeconds, sessionData)) {
81+
Logger.getLogger().w("Failed to initialize Crashlytics NDK for session " + sessionId);
82+
}
83+
};
84+
85+
if (installHandlerDuringPrepareSession) {
86+
signalHandlerInstaller.installHandler();
6187
}
6288
}
6389

@@ -77,4 +103,43 @@ public NativeSessionFileProvider getSessionFileProvider(@NonNull String sessionI
77103
// equivalent objects.
78104
return new SessionFilesProvider(controller.getFilesForSession(sessionId));
79105
}
106+
107+
/**
108+
* Installs the native signal handler, if the session has already been prepared. Otherwise,
109+
* calling this method will result in the native signal handler being installed as soon as the
110+
* session is prepared. Used by Firebase Crashlytics for Unity.
111+
*/
112+
public synchronized void installSignalHandler() {
113+
// If the handler is already initialized, execute it immediately.
114+
// Otherwise, set installHandlerDuringPrepareSession=true so it will be installed as soon as it
115+
// is available.
116+
if (signalHandlerInstaller != null) {
117+
signalHandlerInstaller.installHandler();
118+
return;
119+
}
120+
if (installHandlerDuringPrepareSession) {
121+
// If installHandlerDuringPrepareSession is already true, we can no-op. The signal handler
122+
// was likely already installed (during prep). This method probably should not have been
123+
// called, so log a warning.
124+
Logger.getLogger().w("Native signal handler already installed; skipping re-install.");
125+
} else {
126+
Logger.getLogger()
127+
.d(
128+
"Deferring signal handler installation until the FirebaseCrashlyticsNdk session has been prepared");
129+
installHandlerDuringPrepareSession = true;
130+
}
131+
}
132+
133+
/**
134+
* Gets the singleton {@link FirebaseCrashlyticsNdk} instance. Used by Firebase Unity.
135+
*
136+
* @throws NullPointerException if create() has not already been called.
137+
*/
138+
@NonNull
139+
public static FirebaseCrashlyticsNdk getInstance() {
140+
if (instance == null) {
141+
throw new NullPointerException("FirebaseCrashlyticsNdk component is not present.");
142+
}
143+
return instance;
144+
}
80145
}

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/CrashlyticsNativeComponentDeferredProxyTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ public void whenAvailable(
6363
0, "model", 1, 1000, 2000, false, 0, "manufacturer", "modelClass");
6464
StaticSessionData sessionData = StaticSessionData.create(appData, osData, deviceData);
6565

66-
proxy.openSession(TEST_SESSION_ID, TEST_GENERATOR, TEST_START_TIME, sessionData);
66+
proxy.prepareNativeSession(TEST_SESSION_ID, TEST_GENERATOR, TEST_START_TIME, sessionData);
6767
Mockito.verify(component, Mockito.times(1))
68-
.openSession(eq(TEST_SESSION_ID), eq(TEST_GENERATOR), eq(TEST_START_TIME), eq(sessionData));
68+
.prepareNativeSession(
69+
eq(TEST_SESSION_ID), eq(TEST_GENERATOR), eq(TEST_START_TIME), eq(sessionData));
6970

7071
proxy.finalizeSession(TEST_SESSION_ID);
7172
Mockito.verify(component, Mockito.times(1)).finalizeSession(eq(TEST_SESSION_ID));

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/CrashlyticsNativeComponent.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ public interface CrashlyticsNativeComponent {
2121

2222
boolean hasCrashDataForSession(@NonNull String sessionId);
2323

24-
void openSession(
24+
/**
25+
* Prepares the native session for opening. Whether or not Crashlytics is fully initialized to
26+
* handle native symbols is implementation dependent.
27+
*/
28+
void prepareNativeSession(
2529
@NonNull String sessionId,
2630
@NonNull String generator,
2731
long startedAtSeconds,

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/CrashlyticsNativeComponentDeferredProxy.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public boolean hasCrashDataForSession(@NonNull String sessionId) {
4747
}
4848

4949
@Override
50-
public void openSession(
50+
public void prepareNativeSession(
5151
@NonNull String sessionId,
5252
@NonNull String generator,
5353
long startedAtSeconds,
@@ -57,7 +57,9 @@ public void openSession(
5757

5858
this.deferredNativeComponent.whenAvailable(
5959
nativeComponent -> {
60-
nativeComponent.get().openSession(sessionId, generator, startedAtSeconds, sessionData);
60+
nativeComponent
61+
.get()
62+
.prepareNativeSession(sessionId, generator, startedAtSeconds, sessionData);
6163
});
6264
}
6365

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ public class CommonUtils {
6767
"com.google.firebase.crashlytics.mapping_file_id";
6868
static final String LEGACY_MAPPING_FILE_ID_RESOURCE_NAME = "com.crashlytics.android.build_id";
6969

70-
private static final String UNITY_EDITOR_VERSION =
71-
"com.google.firebase.crashlytics.unity_version";
72-
7370
private static final long UNCALCULATED_TOTAL_RAM = -1;
7471
static final int BYTES_IN_A_GIGABYTE = 1073741824;
7572
static final int BYTES_IN_A_MEGABYTE = 1048576;
@@ -601,20 +598,6 @@ public static String getMappingFileId(Context context) {
601598
return mappingFileId;
602599
}
603600

604-
/**
605-
* @return the Unity Editor version resolved from String resources, or <code>null</code> if the
606-
* value was not present.
607-
*/
608-
public static String resolveUnityEditorVersion(Context context) {
609-
String version = null;
610-
final int id = CommonUtils.getResourcesIdentifier(context, UNITY_EDITOR_VERSION, "string");
611-
if (id != 0) {
612-
version = context.getResources().getString(id);
613-
Logger.getLogger().v("Unity Editor version is: " + version);
614-
}
615-
return version;
616-
}
617-
618601
public static void closeQuietly(Closeable closeable) {
619602
if (closeable != null) {
620603
try {

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ private void doOpenSession() {
577577
StaticSessionData.OsData osData = createOsData(getContext());
578578
StaticSessionData.DeviceData deviceData = createDeviceData(getContext());
579579

580-
nativeComponent.openSession(
580+
nativeComponent.prepareNativeSession(
581581
sessionIdentifier,
582582
generator,
583583
startedAtSeconds,

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/unity/ResourceUnityVersionProvider.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,43 @@
1515
package com.google.firebase.crashlytics.internal.unity;
1616

1717
import android.content.Context;
18+
import com.google.firebase.crashlytics.internal.Logger;
1819
import com.google.firebase.crashlytics.internal.common.CommonUtils;
1920

2021
public class ResourceUnityVersionProvider implements UnityVersionProvider {
2122

23+
private static final String UNITY_EDITOR_VERSION =
24+
"com.google.firebase.crashlytics.unity_version";
25+
26+
private static boolean isUnityVersionSet = false;
27+
private static String unityVersion = null;
28+
2229
private final Context context;
2330

24-
private boolean hasRead = false;
25-
private String unityVersion;
31+
/**
32+
* @return the Unity Editor version resolved from String resources, or <code>null</code> if the
33+
* value was not present. This method can be invoked directly; access via the instance method
34+
* from UnityVersionProvider is provided to support mocking while testing.
35+
*/
36+
public static synchronized String resolveUnityEditorVersion(Context context) {
37+
if (isUnityVersionSet) {
38+
return unityVersion;
39+
}
40+
final int id = CommonUtils.getResourcesIdentifier(context, UNITY_EDITOR_VERSION, "string");
41+
if (id != 0) {
42+
unityVersion = context.getResources().getString(id);
43+
isUnityVersionSet = true;
44+
Logger.getLogger().v("Unity Editor version is: " + unityVersion);
45+
}
46+
return unityVersion;
47+
}
2648

2749
public ResourceUnityVersionProvider(Context context) {
2850
this.context = context;
2951
}
3052

3153
@Override
3254
public String getUnityVersion() {
33-
if (!hasRead) {
34-
unityVersion = CommonUtils.resolveUnityEditorVersion(context);
35-
hasRead = true;
36-
}
37-
if (unityVersion != null) {
38-
return unityVersion;
39-
}
40-
return null;
55+
return resolveUnityEditorVersion(this.context);
4156
}
4257
}

0 commit comments

Comments
 (0)