From a1660c3eb6bad039725e6e9d3033bfc0d9398b5d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 4 Jun 2026 17:20:04 -0700 Subject: [PATCH 1/5] fix(android): [SDK-4736] move Flutter bridge off main thread Co-authored-by: Cursor --- .../flutter/FlutterMessengerResponder.java | 27 +++++++++++++++++++ .../flutter/OneSignalInAppMessages.java | 11 +++++++- .../onesignal/flutter/OneSignalLocation.java | 11 +++++++- .../flutter/OneSignalNotifications.java | 26 +++++++++++++----- .../onesignal/flutter/OneSignalPlugin.java | 11 +++++++- .../flutter/OneSignalPushSubscription.java | 11 +++++++- .../onesignal/flutter/OneSignalSession.java | 11 +++++++- .../com/onesignal/flutter/OneSignalUser.java | 11 +++++++- 8 files changed, 106 insertions(+), 13 deletions(-) diff --git a/android/src/main/java/com/onesignal/flutter/FlutterMessengerResponder.java b/android/src/main/java/com/onesignal/flutter/FlutterMessengerResponder.java index adf21ea1..cc904638 100644 --- a/android/src/main/java/com/onesignal/flutter/FlutterMessengerResponder.java +++ b/android/src/main/java/com/onesignal/flutter/FlutterMessengerResponder.java @@ -3,12 +3,25 @@ import android.content.Context; import android.os.Handler; import android.os.Looper; +import com.onesignal.debug.internal.logging.Logging; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; abstract class FlutterMessengerResponder { + private static final ExecutorService BACKGROUND_EXECUTOR = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "OneSignalFlutterBg"); + thread.setDaemon(true); + return thread; + } + }); + Context context; protected MethodChannel channel; BinaryMessenger messenger; @@ -104,6 +117,20 @@ private void runOnMainThread(final Runnable runnable) { } } + void runOnBackgroundThread(final MethodChannel.Result result, final Runnable runnable) { + BACKGROUND_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } catch (Exception e) { + Logging.error("Encountered an error while handling a Flutter method call: " + e.toString(), e); + replyError(result, "OneSignal", e.getMessage(), null); + } + } + }); + } + void invokeMethodOnUiThread(final String methodName, final HashMap map) { // final MethodChannel channel = this.channel; runOnMainThread(new Runnable() { diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalInAppMessages.java b/android/src/main/java/com/onesignal/flutter/OneSignalInAppMessages.java index c23f5634..dd7789a1 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalInAppMessages.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalInAppMessages.java @@ -40,7 +40,16 @@ void onAttachedToActivity(BinaryMessenger activityMessenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#addTrigger")) this.addTriggers(call, result); else if (call.method.contentEquals("OneSignal#addTriggers")) this.addTriggers(call, result); else if (call.method.contentEquals("OneSignal#removeTrigger")) this.removeTrigger(call, result); diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalLocation.java b/android/src/main/java/com/onesignal/flutter/OneSignalLocation.java index b209c6f4..742bba15 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalLocation.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalLocation.java @@ -28,7 +28,16 @@ static void registerWith(BinaryMessenger messenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#requestPermission")) this.requestPermission(result); else if (call.method.contentEquals("OneSignal#setShared")) this.setShared(call, result); else if (call.method.contentEquals("OneSignal#isShared")) diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java index 8913dc22..24914816 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java @@ -16,6 +16,7 @@ import io.flutter.plugin.common.MethodChannel.Result; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.Dispatchers; @@ -26,8 +27,9 @@ public class OneSignalNotifications extends FlutterMessengerResponder implements MethodCallHandler, INotificationClickListener, INotificationLifecycleListener, IPermissionObserver { private static OneSignalNotifications sharedInstance; - private final HashMap notificationOnWillDisplayEventCache = new HashMap<>(); - private final HashMap preventedDefaultCache = new HashMap<>(); + private final Map notificationOnWillDisplayEventCache = + new ConcurrentHashMap<>(); + private final Map preventedDefaultCache = new ConcurrentHashMap<>(); // #1138: tracks if Dart requested clicks, so we can queue (not drop) them // while the channel is detached across engine/activity lifecycles. @@ -81,7 +83,21 @@ static void registerWith(BinaryMessenger messenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + if (call.method.contentEquals("OneSignal#displayNotification")) this.displayNotification(call, result); + else if (call.method.contentEquals("OneSignal#preventDefault")) this.preventDefault(call, result); + else if (call.method.contentEquals("OneSignal#proceedWithWillDisplay")) + this.proceedWithWillDisplay(call, result); + else + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#permission")) replySuccess(result, OneSignal.getNotifications().getPermission()); else if (call.method.contentEquals("OneSignal#canRequest")) @@ -91,11 +107,7 @@ else if (call.method.contentEquals("OneSignal#canRequest")) else if (call.method.contentEquals("OneSignal#removeGroupedNotifications")) this.removeGroupedNotifications(call, result); else if (call.method.contentEquals("OneSignal#clearAll")) this.clearAll(call, result); - else if (call.method.contentEquals("OneSignal#displayNotification")) this.displayNotification(call, result); - else if (call.method.contentEquals("OneSignal#preventDefault")) this.preventDefault(call, result); else if (call.method.contentEquals("OneSignal#lifecycleInit")) this.lifecycleInit(result); - else if (call.method.contentEquals("OneSignal#proceedWithWillDisplay")) - this.proceedWithWillDisplay(call, result); else if (call.method.contentEquals("OneSignal#addNativeClickListener")) this.registerClickListener(); else replyNotImplemented(result); } diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java index fb3a39b2..64b01a97 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalPlugin.java @@ -86,7 +86,16 @@ public void onDetachedFromActivityForConfigChanges() { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#initialize")) this.initWithContext(call, result); else if (call.method.contentEquals("OneSignal#consentRequired")) this.setConsentRequired(call, result); else if (call.method.contentEquals("OneSignal#consentGiven")) this.setConsentGiven(call, result); diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalPushSubscription.java b/android/src/main/java/com/onesignal/flutter/OneSignalPushSubscription.java index 498d0eae..eb65e898 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalPushSubscription.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalPushSubscription.java @@ -33,7 +33,16 @@ void onAttachedToActivity(BinaryMessenger activityMessenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#optIn")) this.optIn(call, result); else if (call.method.contentEquals("OneSignal#optOut")) this.optOut(call, result); else if (call.method.contentEquals("OneSignal#pushSubscriptionId")) diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalSession.java b/android/src/main/java/com/onesignal/flutter/OneSignalSession.java index ae3a3637..51f94c71 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalSession.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalSession.java @@ -27,7 +27,16 @@ static void registerWith(BinaryMessenger messenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#addOutcome")) this.addOutcome(call, result); else if (call.method.contentEquals("OneSignal#addUniqueOutcome")) this.addUniqueOutcome(call, result); else if (call.method.contentEquals("OneSignal#addOutcomeWithValue")) this.addOutcomeWithValue(call, result); diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalUser.java b/android/src/main/java/com/onesignal/flutter/OneSignalUser.java index 5a4f7995..f5facb0b 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalUser.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalUser.java @@ -34,7 +34,16 @@ void onAttachedToActivity(BinaryMessenger activityMessenger) { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(final MethodCall call, final Result result) { + runOnBackgroundThread(result, new Runnable() { + @Override + public void run() { + handleMethodCall(call, result); + } + }); + } + + private void handleMethodCall(MethodCall call, Result result) { if (call.method.contentEquals("OneSignal#setLanguage")) this.setLanguage(call, result); else if (call.method.contentEquals("OneSignal#getOnesignalId")) this.getOnesignalId(call, result); else if (call.method.contentEquals("OneSignal#getExternalId")) this.getExternalId(call, result); From a0cf574ee08f8a756f326a169d724c84b1c9c27f Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 4 Jun 2026 17:30:29 -0700 Subject: [PATCH 2/5] chore(examples): [SDK-4736] migrate iOS to scene-based lifecycle --- examples/demo/android/gradle.properties | 4 +++ .../demo/ios/Flutter/AppFrameworkInfo.plist | 2 -- examples/demo/ios/Runner/AppDelegate.swift | 7 +++-- examples/demo/ios/Runner/Info.plist | 29 ++++++++++++++++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/examples/demo/android/gradle.properties b/examples/demo/android/gradle.properties index fbee1d8c..d5da7278 100644 --- a/examples/demo/android/gradle.properties +++ b/examples/demo/android/gradle.properties @@ -1,2 +1,6 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/examples/demo/ios/Flutter/AppFrameworkInfo.plist b/examples/demo/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/examples/demo/ios/Flutter/AppFrameworkInfo.plist +++ b/examples/demo/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/examples/demo/ios/Runner/AppDelegate.swift b/examples/demo/ios/Runner/AppDelegate.swift index 62666446..c30b367e 100644 --- a/examples/demo/ios/Runner/AppDelegate.swift +++ b/examples/demo/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/examples/demo/ios/Runner/Info.plist b/examples/demo/ios/Runner/Info.plist index fd8aee7d..fc79bc00 100644 --- a/examples/demo/ios/Runner/Info.plist +++ b/examples/demo/ios/Runner/Info.plist @@ -26,12 +26,33 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - NSSupportsLiveActivities - - NSLocationWhenInUseUsageDescription - This app uses your location to personalize notifications and content. NSLocationAlwaysAndWhenInUseUsageDescription This app uses your location to personalize notifications and content, even in the background. + NSLocationWhenInUseUsageDescription + This app uses your location to personalize notifications and content. + NSSupportsLiveActivities + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UIBackgroundModes From ff12e69a09f753760eee821d278442d95f5ad283 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 4 Jun 2026 17:32:01 -0700 Subject: [PATCH 3/5] fix(android): [SDK-4736] make clickListenerRequested volatile --- .../main/java/com/onesignal/flutter/OneSignalNotifications.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java index 24914816..34db99ce 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java @@ -33,7 +33,7 @@ public class OneSignalNotifications extends FlutterMessengerResponder // #1138: tracks if Dart requested clicks, so we can queue (not drop) them // while the channel is detached across engine/activity lifecycles. - private boolean clickListenerRequested = false; + private volatile boolean clickListenerRequested = false; public static OneSignalNotifications getSharedInstance() { if (sharedInstance == null) { From d015076095571e160d044173cb847f6e6deff130 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 9 Jun 2026 15:31:39 -0700 Subject: [PATCH 4/5] fix(android): [SDK-4736] document foreground notification threading Co-authored-by: Cursor --- .../main/java/com/onesignal/flutter/OneSignalNotifications.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java index 34db99ce..31bc35be 100644 --- a/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java +++ b/android/src/main/java/com/onesignal/flutter/OneSignalNotifications.java @@ -84,6 +84,8 @@ static void registerWith(BinaryMessenger messenger) { @Override public void onMethodCall(final MethodCall call, final Result result) { + // These paths only use cached foreground events and must not wait behind + // SDK calls that can block during initialization. if (call.method.contentEquals("OneSignal#displayNotification")) this.displayNotification(call, result); else if (call.method.contentEquals("OneSignal#preventDefault")) this.preventDefault(call, result); else if (call.method.contentEquals("OneSignal#proceedWithWillDisplay")) From 20eaef1a74708f309b5b8db0e16f314c95e8cba5 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 9 Jun 2026 15:52:53 -0700 Subject: [PATCH 5/5] docs(examples): [SDK-4736] clarify foreground notification API --- examples/demo/lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/demo/lib/main.dart b/examples/demo/lib/main.dart index 9075802c..223823be 100644 --- a/examples/demo/lib/main.dart +++ b/examples/demo/lib/main.dart @@ -70,7 +70,8 @@ Future main() async { debugPrint( 'Notification foreground will display: ${event.notification.title}', ); - event.notification.display(); + // event.preventDefault(); // This will prevent the notification from being displayed + // event.notification.display(); // This will override the preventDefault and display the notification }); // Set up API service