Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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
45 changes: 45 additions & 0 deletions examples/demo_fm/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: ios
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: linux
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: macos
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: web
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: windows
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
Loading
Loading