Skip to content

Commit 8b2fd2d

Browse files
committed
fix(android): track Activity early to fix cold-start init
1 parent 8743251 commit 8b2fd2d

7 files changed

Lines changed: 144 additions & 17 deletions

File tree

android/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ android {
2727
dependencies {
2828
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
2929

30+
// androidx.startup runs our OneSignalInitializer during Application.onCreate so we can register
31+
// an ActivityLifecycleCallbacks before MainActivity.onResume fires. This avoids the cold-start
32+
// race where ReactApplicationContext.getCurrentActivity() returns null and the OneSignal SDK
33+
// ends up holding an ApplicationContext instead of the real Activity.
34+
implementation 'androidx.startup:startup-runtime:1.1.1'
35+
3036
// api is used instead of implementation so the parent :app project can access any of the OneSignal Java
3137
// classes if needed. Such as com.onesignal.NotificationExtenderService
3238
//
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
3+
<application>
4+
<provider
5+
android:name="androidx.startup.InitializationProvider"
6+
android:authorities="${applicationId}.androidx-startup"
7+
android:exported="false"
8+
tools:node="merge">
9+
<meta-data
10+
android:name="com.onesignal.rnonesignalandroid.OneSignalInitializer"
11+
android:value="androidx.startup" />
12+
</provider>
13+
</application>
214
</manifest>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.onesignal.rnonesignalandroid;
2+
3+
import android.app.Activity;
4+
import android.app.Application;
5+
import android.os.Bundle;
6+
import androidx.annotation.Nullable;
7+
import java.lang.ref.WeakReference;
8+
9+
/**
10+
* Tracks the host app's current Activity from Application.onCreate onward.
11+
*
12+
* <p>Registered very early via {@link OneSignalInitializer} (androidx.startup) so it captures the
13+
* first {@code MainActivity.onResume} that fires before the React Native bridge has loaded the JS
14+
* bundle. Without this, {@link com.facebook.react.bridge.ReactApplicationContext#getCurrentActivity()}
15+
* frequently returns {@code null} during cold start in bridgeless mode, causing
16+
* {@code RNOneSignal.initialize} to hand the OneSignal SDK an ApplicationContext instead of the
17+
* real Activity. That in turn leaves {@code ApplicationService.current == null} and queues
18+
* {@code requestPermission()} until the next foreground.
19+
*/
20+
public class ActivityLifecycleTracker implements Application.ActivityLifecycleCallbacks {
21+
private static final ActivityLifecycleTracker INSTANCE = new ActivityLifecycleTracker();
22+
23+
private WeakReference<Activity> currentActivity = new WeakReference<>(null);
24+
25+
private ActivityLifecycleTracker() {}
26+
27+
public static ActivityLifecycleTracker getInstance() {
28+
return INSTANCE;
29+
}
30+
31+
@Nullable
32+
public Activity getCurrentActivity() {
33+
return currentActivity.get();
34+
}
35+
36+
@Override
37+
public void onActivityCreated(Activity activity, @Nullable Bundle savedInstanceState) {
38+
currentActivity = new WeakReference<>(activity);
39+
}
40+
41+
@Override
42+
public void onActivityStarted(Activity activity) {
43+
currentActivity = new WeakReference<>(activity);
44+
}
45+
46+
@Override
47+
public void onActivityResumed(Activity activity) {
48+
currentActivity = new WeakReference<>(activity);
49+
}
50+
51+
@Override
52+
public void onActivityPaused(Activity activity) {
53+
// Intentionally no-op: keep the reference so a transient overlay (e.g. permission dialog,
54+
// PermissionsActivity) doesn't blank out the current Activity for callers that race with it.
55+
}
56+
57+
@Override
58+
public void onActivityStopped(Activity activity) {
59+
// Intentionally no-op for the same reason as onActivityPaused.
60+
}
61+
62+
@Override
63+
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
64+
65+
@Override
66+
public void onActivityDestroyed(Activity activity) {
67+
Activity current = currentActivity.get();
68+
if (current == activity) {
69+
currentActivity = new WeakReference<>(null);
70+
}
71+
}
72+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.onesignal.rnonesignalandroid;
2+
3+
import android.app.Application;
4+
import android.content.Context;
5+
import androidx.annotation.NonNull;
6+
import androidx.startup.Initializer;
7+
import java.util.Collections;
8+
import java.util.List;
9+
10+
/**
11+
* androidx.startup entry point that registers {@link ActivityLifecycleTracker} against the host
12+
* {@link Application} during {@code Application.onCreate}, before any Activity is created.
13+
*
14+
* <p>This does NOT initialize the OneSignal SDK itself: the App ID is supplied at runtime by JS
15+
* via {@code OneSignal.initialize(appId)}. The job here is purely to capture the current Activity
16+
* early so that when JS later calls initialize, {@code RNOneSignal} can hand a real Activity to
17+
* {@code OneSignal.initWithContext}.
18+
*/
19+
public class OneSignalInitializer implements Initializer<ActivityLifecycleTracker> {
20+
21+
@NonNull
22+
@Override
23+
public ActivityLifecycleTracker create(@NonNull Context context) {
24+
ActivityLifecycleTracker tracker = ActivityLifecycleTracker.getInstance();
25+
Context appContext = context.getApplicationContext();
26+
if (appContext instanceof Application) {
27+
((Application) appContext).registerActivityLifecycleCallbacks(tracker);
28+
}
29+
return tracker;
30+
}
31+
32+
@NonNull
33+
@Override
34+
public List<Class<? extends Initializer<?>>> dependencies() {
35+
return Collections.emptyList();
36+
}
37+
}

android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,20 @@ public void initialize(String appId) {
238238
}
239239

240240
ReactApplicationContext reactContext = getReactApplicationContext();
241-
Context context = reactContext.getCurrentActivity();
241+
// Prefer the Activity captured by ActivityLifecycleTracker (registered via androidx.startup
242+
// before MainActivity.onResume), then fall back to ReactApplicationContext's accessor and
243+
// finally the ApplicationContext. Passing the real Activity lets the OneSignal SDK populate
244+
// ApplicationService.current immediately, so requestPermission() can launch the OS dialog
245+
// on the first cold-start instead of waiting for the next foreground event.
246+
Context context = ActivityLifecycleTracker.getInstance().getCurrentActivity();
247+
if (context == null) {
248+
context = reactContext.getCurrentActivity();
249+
}
242250
if (context == null) {
243251
context = reactContext.getApplicationContext();
244252
}
245253

254+
Logging.debug("OneSignal initialize using context: " + context.getClass().getSimpleName(), null);
246255
OneSignal.initWithContext(context, appId);
247256
oneSignalInitDone = true;
248257
}

examples/demo/bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/demo/src/screens/HomeScreen.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useFocusEffect, useNavigation } from '@react-navigation/native';
2-
import React, { useCallback, useRef, useState } from 'react';
1+
import { useNavigation } from '@react-navigation/native';
2+
import React, { useEffect, useState } from 'react';
33
import { Platform, ScrollView, StyleSheet, View } from 'react-native';
44

55
import ActionButton from '../components/ActionButton';
@@ -32,18 +32,9 @@ export default function HomeScreen() {
3232
const [tooltipVisible, setTooltipVisible] = useState(false);
3333
const [activeTooltip, setActiveTooltip] = useState<TooltipData | null>(null);
3434

35-
// Prompt for push only after the screen is actually focused so the Android
36-
// Activity is resumed and can present the OS dialog. Otherwise the request
37-
// gets queued and the prompt only appears after the next foreground.
38-
const hasPromptedRef = useRef(false);
39-
useFocusEffect(
40-
useCallback(() => {
41-
if (os.isReady && !hasPromptedRef.current) {
42-
hasPromptedRef.current = true;
43-
os.promptPush();
44-
}
45-
}, [os.isReady, os.promptPush]),
46-
);
35+
useEffect(() => {
36+
if (os.isReady) os.promptPush();
37+
}, [os.isReady, os.promptPush]);
4738

4839
const showTooltipModal = (key: string) => {
4940
const tooltip = TooltipHelper.getInstance().getTooltip(key);

0 commit comments

Comments
 (0)