Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ dlcov.log
**/Package.resolved
**/.lock
**/workspace-state.json

# Per-developer Firebase config for issue #1138 repro scaffolding
# (only needed because firebase_messaging is wired into examples/demo;
# OneSignal itself does NOT need this file).
Comment thread
fadi-george marked this conversation as resolved.
**/google-services.json
**/GoogleService-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,55 @@
import android.os.Looper;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import java.util.HashMap;

abstract class FlutterMessengerResponder {
Context context;
protected MethodChannel channel;
BinaryMessenger messenger;

/**
* #1138: bind the outgoing shared channel only on the first engine. These
* responders are process-global singletons but {@code registerWith} runs once
* per Flutter engine; FlutterFire's headless background engine would otherwise
* rebind the channel to an isolate with no listeners and drop native callbacks.
*
* <p>The incoming-call handler is still registered on every engine's messenger
* so Dart->Native calls work from any isolate (e.g. an FCM background handler),
* matching the pre-#1138 behavior; only the outgoing Native->Dart channel stays
* pinned to the first engine.
*
* @return true if this call performed the initial bind.
*/
boolean bindChannelIfUnbound(BinaryMessenger messenger, String channelName, MethodCallHandler handler) {
MethodChannel channel = new MethodChannel(messenger, channelName);
channel.setMethodCallHandler(handler);
if (this.channel != null) {
return false;
}
this.messenger = messenger;
this.channel = channel;
return true;
}

/**
* #1138: reassert the channel binding to the engine that hosts the activity (the
* UI isolate), in case a background engine attached after us. No-op if the
* messenger is unchanged.
*
* @return true if the channel was rebound to a different engine.
*/
boolean rebindChannelToEngine(BinaryMessenger activityMessenger, String channelName, MethodCallHandler handler) {
if (activityMessenger == null || activityMessenger == this.messenger) {
return false;
}
this.messenger = activityMessenger;
this.channel = new MethodChannel(activityMessenger, channelName);
this.channel.setMethodCallHandler(handler);
return true;
}

/**
* MethodChannel class is home to success() method used by Result class
* It has the @UiThread annotation and must be run on UI thread, otherwise a RuntimeException will be thrown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.onesignal.inAppMessages.IInAppMessageWillDisplayEvent;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import java.util.Collection;
Expand All @@ -33,10 +32,11 @@ private OneSignalInAppMessages() {}

static void registerWith(BinaryMessenger messenger) {
OneSignalInAppMessages controller = getSharedInstance();
controller.bindChannelIfUnbound(messenger, "OneSignal#inappmessages", controller);
}

controller.messenger = messenger;
controller.channel = new MethodChannel(messenger, "OneSignal#inappmessages");
controller.channel.setMethodCallHandler(controller);
void onAttachedToActivity(BinaryMessenger activityMessenger) {
rebindChannelToEngine(activityMessenger, "OneSignal#inappmessages", this);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public class OneSignalNotifications extends FlutterMessengerResponder
private final HashMap<String, INotificationWillDisplayEvent> notificationOnWillDisplayEventCache = new HashMap<>();
private final HashMap<String, INotificationWillDisplayEvent> preventedDefaultCache = new HashMap<>();

// #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;

public static OneSignalNotifications getSharedInstance() {
if (sharedInstance == null) {
sharedInstance = new OneSignalNotifications();
Expand Down Expand Up @@ -73,9 +77,7 @@ public void resumeWith(@NonNull Object o) {

static void registerWith(BinaryMessenger messenger) {
OneSignalNotifications controller = getSharedInstance();
controller.messenger = messenger;
controller.channel = new MethodChannel(messenger, "OneSignal#notifications");
controller.channel.setMethodCallHandler(controller);
controller.bindChannelIfUnbound(messenger, "OneSignal#notifications", controller);
}

@Override
Expand Down Expand Up @@ -227,18 +229,53 @@ public void onNotificationPermissionChange(boolean permission) {
invokeMethodOnUiThread("OneSignal#onNotificationPermissionDidChange", hash);
}

void onDetachedFromEngine() {
// The Flutter engine can be torn down before OneSignal.initialize() has been
// called from Dart (cold start, fast finish, etc.). Calling getNotifications()
// in that state throws IllegalStateException from the native SDK. See #1149.
void onDetachedFromEngine(BinaryMessenger detachingMessenger) {
// #1138: ignore a FlutterFire background engine detaching — removing the
// listener bound to the live UI engine would drop the next click (the UI
// engine fires no activity event, so nothing re-adds it).
if (detachingMessenger != null && detachingMessenger != this.messenger) {
return;
}
// #1149: engine can be torn down before Dart calls initialize().
if (!OneSignal.isInitialized()) {
return;
}
// Unsubscribe so clicks while the engine is dead get queued by the native SDK
// instead of dispatched on a detached channel.
// Unsubscribe so clicks get queued by the native SDK, not dropped.
OneSignal.getNotifications().removeClickListener(this);
}

/**
* Same as {@link #onDetachedFromEngine} but for when the engine survives and
* only the host activity is destroyed (e.g. back-pressed out of MainActivity).
*/
void onDetachedFromActivity() {
if (!OneSignal.isInitialized()) {
Comment thread
claude[bot] marked this conversation as resolved.
return;
Comment thread
claude[bot] marked this conversation as resolved.
}
OneSignal.getNotifications().removeClickListener(this);
}

/**
* #1138: rebind the shared channel to the UI engine on (re)attach and drain
* any clicks the native SDK queued while detached.
*/
void onAttachedToActivity(BinaryMessenger activityMessenger) {
// Rebind the shared channel so callbacks hit the now-foreground engine.
rebindChannelToEngine(activityMessenger, "OneSignal#notifications", this);
// Re-add the listener so the native SDK drains any clicks queued while
// detached. Works for fresh, FCM-background, and pre-warmed cached engines
// alike: a pre-warmed engine's Dart already ran main() and won't re-call
// OneSignal#addNativeClickListener, so the rebind alone wouldn't restore it.
// Draining before this engine's Dart listeners exist is safe — the Dart
// bridge buffers clicks that arrive with no listeners and flushes them once
// addClickListener runs.
if (!clickListenerRequested || !OneSignal.isInitialized()) {
return;
}
OneSignal.getNotifications().removeClickListener(this);
OneSignal.getNotifications().addClickListener(this);
}
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
fadi-george marked this conversation as resolved.

private void lifecycleInit(Result result) {
OneSignal.getNotifications().removeForegroundLifecycleListener(this);
OneSignal.getNotifications().addForegroundLifecycleListener(this);
Expand All @@ -250,6 +287,7 @@ private void lifecycleInit(Result result) {
}

private void registerClickListener() {
clickListenerRequested = true;
OneSignal.getNotifications().removeClickListener(this);
OneSignal.getNotifications().addClickListener(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,45 @@ public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding flutt

@Override
public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
onDetachedFromEngine();
}

private void onDetachedFromEngine() {
OneSignalNotifications.getSharedInstance().onDetachedFromEngine();
// #1138: pass the detaching engine's messenger so a background (FlutterFire)
// engine detaching doesn't tear down the listener bound to the UI engine.
OneSignalNotifications.getSharedInstance().onDetachedFromEngine(binding.getBinaryMessenger());
}

@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
this.context = binding.getActivity();
rebindChannelsToActivityEngine();
}

@Override
public void onDetachedFromActivity() {}
public void onDetachedFromActivity() {
// #1138: unregister so the native SDK queues clicks until a new activity attaches.
OneSignalNotifications.getSharedInstance().onDetachedFromActivity();
}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {}
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
this.context = binding.getActivity();
rebindChannelsToActivityEngine();
}

/**
* #1138: (re)bind the process-global singleton channels to the engine that
* hosts the activity (the UI isolate), so native callbacks aren't routed to a
* FlutterFire background engine that has no listeners.
*/
private void rebindChannelsToActivityEngine() {
OneSignalNotifications.getSharedInstance().onAttachedToActivity(this.messenger);
OneSignalUser.getSharedInstance().onAttachedToActivity(this.messenger);
OneSignalPushSubscription.getSharedInstance().onAttachedToActivity(this.messenger);
OneSignalInAppMessages.getSharedInstance().onAttachedToActivity(this.messenger);
}
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.

@Override
public void onDetachedFromActivityForConfigChanges() {}
public void onDetachedFromActivityForConfigChanges() {
OneSignalNotifications.getSharedInstance().onDetachedFromActivity();
}

@Override
public void onMethodCall(MethodCall call, Result result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.onesignal.user.subscriptions.PushSubscriptionChangedState;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import org.json.JSONException;
Expand All @@ -26,9 +25,11 @@ private OneSignalPushSubscription() {}

static void registerWith(BinaryMessenger messenger) {
OneSignalPushSubscription controller = getSharedInstance();
controller.messenger = messenger;
controller.channel = new MethodChannel(messenger, "OneSignal#pushsubscription");
controller.channel.setMethodCallHandler(controller);
controller.bindChannelIfUnbound(messenger, "OneSignal#pushsubscription", controller);
}

void onAttachedToActivity(BinaryMessenger activityMessenger) {
rebindChannelToEngine(activityMessenger, "OneSignal#pushsubscription", this);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.onesignal.user.state.UserChangedState;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import java.util.List;
Expand All @@ -27,9 +26,11 @@ private OneSignalUser() {}

static void registerWith(BinaryMessenger messenger) {
OneSignalUser controller = getSharedInstance();
controller.messenger = messenger;
controller.channel = new MethodChannel(messenger, "OneSignal#user");
controller.channel.setMethodCallHandler(controller);
controller.bindChannelIfUnbound(messenger, "OneSignal#user", controller);
}

void onAttachedToActivity(BinaryMessenger activityMessenger) {
rebindChannelToEngine(activityMessenger, "OneSignal#user", this);
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Future<void> main() async {
debugPrint('IAM did dismiss: ${event.message.messageId}');
});
OneSignal.InAppMessages.addClickListener((event) {
debugPrint('IAM clicked: ${event.result.actionId}');
debugPrint('IAM clicked: ${event.message.messageId}');
});

// Register notification listeners
Expand Down
11 changes: 10 additions & 1 deletion examples/demo/lib/viewmodels/app_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,17 @@ class AppViewModel extends ChangeNotifier {
OneSignal.User.pushSubscription.addObserver((state) {
_pushSubscriptionId = state.current.id;
_pushEnabled = state.current.optedIn;

String fmtToken(String? t) {
if (t == null || t.isEmpty) return 'null';
return t.length > 8 ? '${t.substring(0, 8)}…' : t;
}

debugPrint(
'Push subscription changed: id=${state.current.id}, optedIn=${state.current.optedIn}',
'Push subscription changed: '
'id=${state.previous.id ?? 'null'} → ${state.current.id ?? 'null'}, '
'optedIn=${state.previous.optedIn} → ${state.current.optedIn}, '
'token=${fmtToken(state.previous.token)} → ${fmtToken(state.current.token)}',
);
notifyListeners();
});
Expand Down
6 changes: 6 additions & 0 deletions examples/demo_fm/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ONESIGNAL_APP_ID=your-onesignal-app-id
ONESIGNAL_API_KEY=your-onesignal-api-key

# Optional: Android Notification Channel ID for the WITH SOUND test notification.
# Create one in your OneSignal dashboard under Settings > Android Notification Categories.
ONESIGNAL_ANDROID_CHANNEL_ID=
49 changes: 49 additions & 0 deletions examples/demo_fm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

# Environment
.env
tools/service-account.json
Loading
Loading