Skip to content

Commit 1522bd8

Browse files
authored
fix(messaging,android): fix an issue that could cause duplicate call stack (#18122)
* fix(messaging,android): fix an issue that could cause duplicate call stack * refactor(messaging,android): drop unreachable shellArgs path from background executor Removing the eager background-isolate start left FlutterShellArgs always null: the only remaining caller is the lazy path in FlutterFirebaseMessagingBackgroundService.onCreate(), which goes through the no-arg startBackgroundIsolate() and passed null explicitly. Collapses the shellArgs parameter and its unreachable branch, and drops the two imports that became unused. This does not reopen #4078. That was a ClassCastException from casting FlutterFragmentActivity to FlutterActivity to reach getFlutterShellArgs(); #4341 fixed it by switching to FlutterShellArgs.fromIntent(). With the activity no longer consulted at all, the cast that caused #4078 is gone. * revert(messaging): restore example minSdk = 23 The minSdk = 23 -> flutter.minSdkVersion change was unrelated to the duplicate-call-stack fix, and it breaks the example on the oldest Flutter this package supports. pubspec declares flutter: '>=3.27.0', where FlutterExtension.minSdkVersion is 21, while firebase_messaging's local-config.gradle sets minSdk=23 -- so the manifest merger fails with "minSdkVersion 21 cannot be smaller than version 23 declared in library". CI only runs stable (currently 3.44.6, where the default is 24), so the break would not show up here. Nine other example apps hardcode minSdk = 23 for the same reason. * feat(messaging,android): persist shell args so the lazy background isolate keeps them The eager background-isolate start was the only place with access to an activity, so removing it left the background engine with no shell args at all. Instead of dropping them, capture them at registration -- where an activity is still available -- and persist them next to the callback handles already stored in SharedPreferences. The lazy start in FlutterFirebaseMessagingBackgroundService.onCreate() then restores them. This is strictly wider coverage than before: previously the killed-app path (the common background case) already started the isolate through the no-arg overload with null args, so shell args only applied when an activity happened to be alive. Now they apply in both paths. Stored as a JSON array rather than a String set because arg order is significant. org.json is already used by FlutterFirebaseMessagingStore. Passing null or empty clears the stored value, so args cannot go stale across a launch that has no activity. * revert(messaging,android): drop shell args persistence in favour of manifest flags Reverts the SharedPreferences plumbing added to carry FlutterShellArgs into the lazily-started background isolate, and documents the forward path instead. FlutterShellArgs is @deprecated upstream with a TODO to delete it once engine args via Intent are unsupported (flutter/flutter#180686). Its documented replacement, FlutterEngineFlags, reads flags from <application> metadata in AndroidManifest.xml -- and FlutterLoader applies those inside ensureInitializationComplete, which this executor already calls. So manifest-declared flags reach the background engine with no plugin code at all. FlutterEngineFlags cannot be referenced directly: it first shipped in Flutter 3.44.0, while this package supports flutter: '>=3.27.0'. Naming it would break compilation on 3.27 through 3.43, and CI only runs stable so that would not be caught here. Net effect: no deprecated API, less code, and engine flags handled by the mechanism Flutter is standardising on. * docs(messaging): document Android background isolate engine flags The background isolate runs in its own FlutterEngine started from a service, so command-line engine flags never reached it, and the intent-based mechanism that partially covered this is deprecated upstream (flutter/flutter#180686). Documents the supported replacement: <application> metadata in AndroidManifest.xml, which FlutterLoader applies to every engine it initializes. Notes the Flutter 3.44.0 floor, the mandatory io.flutter.embedding.android. prefix, that most flags are ignored in release builds, and that the command line wins over the manifest. * style(messaging,android): rewrap javadoc to satisfy google-java-format
1 parent 6b1fbc9 commit 1522bd8

4 files changed

Lines changed: 57 additions & 59 deletions

File tree

docs/cloud-messaging/receive.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,40 @@ application state or execute any UI impacting logic. You can, however, perform l
152152
It is also recommended to complete your logic as soon as possible. Running long, intensive tasks impacts device performance
153153
and may cause the OS to terminate the process. If tasks run for longer than 30 seconds, the device may automatically kill the process.
154154

155+
##### Setting engine flags for the background isolate on Android {: #android-background-engine-flags}
156+
157+
On Android the background isolate runs in its own `FlutterEngine`, started by a
158+
background service once a message arrives. Because no activity is involved, engine
159+
flags you pass on the command line to `flutter run` (such as `--trace-startup`) do
160+
not reach it.
161+
162+
To apply a flag to the background isolate, declare it as `<application>` metadata in
163+
`android/app/src/main/AndroidManifest.xml`. Flutter reads this metadata while
164+
initializing any engine, including the background one, so no extra Dart or Android
165+
code is needed:
166+
167+
```xml
168+
<manifest ...>
169+
<application ...>
170+
<!-- Prefix is required, and is always io.flutter.embedding.android. -->
171+
<meta-data
172+
android:name="io.flutter.embedding.android.TraceStartup"
173+
android:value="true" />
174+
</application>
175+
</manifest>
176+
```
177+
178+
A few things to be aware of:
179+
180+
1. Manifest metadata flags require **Flutter 3.44.0 or higher**. On earlier versions
181+
there is no supported way to set engine flags for the background isolate.
182+
2. The `io.flutter.embedding.android.` prefix is mandatory; it exists to avoid
183+
collisions with other metadata keys.
184+
3. Most flags are intended for debugging and are ignored in release builds. When that
185+
happens Flutter logs `Flag with metadata key ... is not allowed in release builds`.
186+
4. If the same flag is set both in the manifest and on the command line, the command
187+
line value wins.
188+
155189
#### Web {:#web}
156190
{:#web}
157191

packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingBackgroundExecutor.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import com.google.firebase.messaging.RemoteMessage;
1717
import io.flutter.FlutterInjector;
1818
import io.flutter.embedding.engine.FlutterEngine;
19-
import io.flutter.embedding.engine.FlutterShellArgs;
2019
import io.flutter.embedding.engine.dart.DartExecutor;
2120
import io.flutter.embedding.engine.dart.DartExecutor.DartCallback;
2221
import io.flutter.embedding.engine.loader.FlutterLoader;
@@ -26,7 +25,6 @@
2625
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
2726
import io.flutter.plugin.common.MethodChannel.Result;
2827
import io.flutter.view.FlutterCallbackInformation;
29-
import java.util.Arrays;
3028
import java.util.HashMap;
3129
import java.util.Map;
3230
import java.util.concurrent.CountDownLatch;
@@ -106,13 +104,22 @@ public void startBackgroundIsolate() {
106104
if (isNotRunning()) {
107105
long callbackHandle = getPluginCallbackHandle();
108106
if (callbackHandle != 0) {
109-
startBackgroundIsolate(callbackHandle, null);
107+
startBackgroundIsolate(callbackHandle);
110108
}
111109
}
112110
}
113111

114-
/** Starts running a background Dart isolate within a new {@link FlutterEngine}. */
115-
public void startBackgroundIsolate(long callbackHandle, FlutterShellArgs shellArgs) {
112+
/**
113+
* Starts running a background Dart isolate within a new {@link FlutterEngine}.
114+
*
115+
* <p>No engine shell args are passed here. They used to be read from the launching activity's
116+
* intent via {@code FlutterShellArgs}, but that class is deprecated and slated for removal (see
117+
* flutter/flutter#180686), and the isolate is now started lazily from a service where no activity
118+
* is available. Engine flags should instead be declared as {@code <application>} metadata in
119+
* AndroidManifest.xml, which {@link FlutterLoader#ensureInitializationComplete} applies on its
120+
* own -- so they reach this background engine without any plugin plumbing.
121+
*/
122+
public void startBackgroundIsolate(long callbackHandle) {
116123
if (backgroundFlutterEngine != null) {
117124
Log.e(TAG, "Background isolate already started.");
118125
return;
@@ -131,19 +138,9 @@ public void startBackgroundIsolate(long callbackHandle, FlutterShellArgs shellAr
131138
String appBundlePath = loader.findAppBundlePath();
132139
AssetManager assets = ContextHolder.getApplicationContext().getAssets();
133140
if (isNotRunning()) {
134-
if (shellArgs != null) {
135-
Log.i(
136-
TAG,
137-
"Creating background FlutterEngine instance, with args: "
138-
+ Arrays.toString(shellArgs.toArray()));
139-
backgroundFlutterEngine =
140-
new FlutterEngine(
141-
ContextHolder.getApplicationContext(), shellArgs.toArray());
142-
} else {
143-
Log.i(TAG, "Creating background FlutterEngine instance.");
144-
backgroundFlutterEngine =
145-
new FlutterEngine(ContextHolder.getApplicationContext());
146-
}
141+
Log.i(TAG, "Creating background FlutterEngine instance.");
142+
backgroundFlutterEngine =
143+
new FlutterEngine(ContextHolder.getApplicationContext());
147144
// We need to create an instance of `FlutterEngine` before looking up the
148145
// callback. If we don't, the callback cache won't be initialized and the
149146
// lookup will fail.

packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingBackgroundService.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import android.os.Handler;
1010
import android.util.Log;
1111
import androidx.annotation.NonNull;
12-
import io.flutter.embedding.engine.FlutterShellArgs;
1312
import java.util.Collections;
1413
import java.util.LinkedList;
1514
import java.util.List;
@@ -37,28 +36,6 @@ public static void enqueueMessageProcessing(
3736
isHighPriority);
3837
}
3938

40-
/**
41-
* Starts the background isolate for the {@link FlutterFirebaseMessagingBackgroundService}.
42-
*
43-
* <p>Preconditions:
44-
*
45-
* <ul>
46-
* <li>The given {@code callbackHandle} must correspond to a registered Dart callback. If the
47-
* handle does not resolve to a Dart callback then this method does nothing.
48-
* <li>A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link
49-
* PluginRegistrantException} will be thrown.
50-
* </ul>
51-
*/
52-
@SuppressWarnings("JavadocReference")
53-
public static void startBackgroundIsolate(long callbackHandle, FlutterShellArgs shellArgs) {
54-
if (flutterBackgroundExecutor != null) {
55-
Log.w(TAG, "Attempted to start a duplicate background isolate. Returning...");
56-
return;
57-
}
58-
flutterBackgroundExecutor = new FlutterFirebaseMessagingBackgroundExecutor();
59-
flutterBackgroundExecutor.startBackgroundIsolate(callbackHandle, shellArgs);
60-
}
61-
6239
/**
6340
* Called once the Dart isolate ({@code flutterBackgroundExecutor}) has finished initializing.
6441
*

packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.google.firebase.FirebaseApp;
2424
import com.google.firebase.messaging.FirebaseMessaging;
2525
import com.google.firebase.messaging.RemoteMessage;
26-
import io.flutter.embedding.engine.FlutterShellArgs;
2726
import io.flutter.embedding.engine.plugins.FlutterPlugin;
2827
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
2928
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@@ -424,12 +423,9 @@ public void onMethodCall(final MethodCall call, @NonNull final Result result) {
424423
Task<?> methodCallTask;
425424

426425
switch (call.method) {
427-
// This message is sent when the Dart side of this plugin is told to initialize.
428-
// In response, this (native) side of the plugin needs to spin up a background
429-
// Dart isolate by using the given pluginCallbackHandle, and then setup a background
430-
// method channel to communicate with the new background isolate. Once completed,
431-
// this onMethodCall() method will receive messages from both the primary and background
432-
// method channels.
426+
// This message is sent when the Dart side of this plugin registers a background
427+
// message handler. We persist the callback handles to SharedPreferences so
428+
// the background service can start the isolate later when a message arrives.
433429
case "Messaging#startBackgroundIsolate":
434430
@SuppressWarnings("unchecked")
435431
Map<String, Object> arguments = ((Map<String, Object>) call.arguments);
@@ -458,19 +454,13 @@ public void onMethodCall(final MethodCall call, @NonNull final Result result) {
458454
"Expected 'Long' or 'Integer' type for 'userCallbackHandle'.");
459455
}
460456

461-
FlutterShellArgs shellArgs = null;
462-
if (mainActivity != null) {
463-
// Supports both Flutter Activity types:
464-
// io.flutter.embedding.android.FlutterFragmentActivity
465-
// io.flutter.embedding.android.FlutterActivity
466-
// We could use `getFlutterShellArgs()` but this is only available on `FlutterActivity`.
467-
shellArgs = FlutterShellArgs.fromIntent(mainActivity.getIntent());
468-
}
469-
457+
// Only save the callback handles to SharedPreferences. Don't start the
458+
// background isolate here — it will be started lazily in
459+
// FlutterFirebaseMessagingBackgroundService.onCreate() when a background
460+
// message actually arrives and the service is started. Starting it eagerly
461+
// caused a duplicate Dart main() to appear in the call stack (#17163).
470462
FlutterFirebaseMessagingBackgroundService.setCallbackDispatcher(pluginCallbackHandle);
471463
FlutterFirebaseMessagingBackgroundService.setUserCallbackHandle(userCallbackHandle);
472-
FlutterFirebaseMessagingBackgroundService.startBackgroundIsolate(
473-
pluginCallbackHandle, shellArgs);
474464
methodCallTask = Tasks.forResult(null);
475465
break;
476466
case "Messaging#getInitialMessage":

0 commit comments

Comments
 (0)