Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-out mechanism for the Android background handler in Firebase Messaging, allowing developers to disable it via manifest metadata. It includes changes to the background executor, service, and receiver to respect this setting, along with comprehensive unit and integration tests. The review feedback highlights three key improvements: ensuring MethodChannel.Result calls in the example app are dispatched to the UI thread to prevent crashes, catching general exceptions when querying the PackageManager to handle unstable Android ROMs, and using a ByteArrayOutputStream when reading ADB shell output to avoid multi-byte UTF-8 decoding errors.
| Thread { | ||
| try { | ||
| result.success( | ||
| probeBackgroundDelivery(messageId, pluginHandle, userHandle), | ||
| ) | ||
| } catch (e: Exception) { | ||
| result.error("PROBE_FAILED", errorMessage(e), null) | ||
| } | ||
| } | ||
| .start() |
There was a problem hiding this comment.
Calling MethodChannel.Result methods (success or error) on a background thread violates Flutter's threading contract and can lead to crashes or undefined behavior. These calls must be dispatched to the platform (UI) thread.
| Thread { | |
| try { | |
| result.success( | |
| probeBackgroundDelivery(messageId, pluginHandle, userHandle), | |
| ) | |
| } catch (e: Exception) { | |
| result.error("PROBE_FAILED", errorMessage(e), null) | |
| } | |
| } | |
| .start() | |
| Thread { | |
| try { | |
| val report = probeBackgroundDelivery(messageId, pluginHandle, userHandle) | |
| runOnUiThread { result.success(report) } | |
| } catch (e: Exception) { | |
| runOnUiThread { result.error("PROBE_FAILED", errorMessage(e), null) } | |
| } | |
| } | |
| .start() |
| } catch (PackageManager.NameNotFoundException exception) { | ||
| Log.w(TAG, "Could not read " + METADATA_KEY + " from the manifest.", exception); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
On some custom or unstable Android ROMs, calling PackageManager methods can throw other runtime exceptions (such as DeadSystemException or general RuntimeException). It is safer to catch Exception generally to prevent unexpected crashes when reading manifest metadata.
| } catch (PackageManager.NameNotFoundException exception) { | |
| Log.w(TAG, "Could not read " + METADATA_KEY + " from the manifest.", exception); | |
| return null; | |
| } | |
| } catch (Exception exception) { | |
| Log.w(TAG, "Could not read " + METADATA_KEY + " from the manifest.", exception); | |
| return null; | |
| } |
| val captured = StringBuilder() | ||
| val buffer = ByteArray(1024) | ||
| while (input.read(buffer) != -1) { | ||
| // discard | ||
| while (true) { | ||
| val read = input.read(buffer) | ||
| if (read < 0) { | ||
| break | ||
| } | ||
| captured.append(String(buffer, 0, read, StandardCharsets.UTF_8)) | ||
| } | ||
| return captured.toString() |
There was a problem hiding this comment.
Converting chunks of a byte array to a String individually can cause decoding errors if a multi-byte UTF-8 character happens to be split across the buffer boundary (e.g., at the 1024-byte mark). Accumulating all bytes in a ByteArrayOutputStream and decoding them at the end is more robust.
| val captured = StringBuilder() | |
| val buffer = ByteArray(1024) | |
| while (input.read(buffer) != -1) { | |
| // discard | |
| while (true) { | |
| val read = input.read(buffer) | |
| if (read < 0) { | |
| break | |
| } | |
| captured.append(String(buffer, 0, read, StandardCharsets.UTF_8)) | |
| } | |
| return captured.toString() | |
| val captured = java.io.ByteArrayOutputStream() | |
| val buffer = ByteArray(1024) | |
| while (true) { | |
| val read = input.read(buffer) | |
| if (read < 0) { | |
| break | |
| } | |
| captured.write(buffer, 0, read) | |
| } | |
| return captured.toString(StandardCharsets.UTF_8.name()) |
| if (action == BackgroundHandlerStartDecision.Action.SKIP_AND_CLEAR) { | ||
| FlutterFirebaseMessagingBackgroundGate.clearHandles(context); | ||
| } | ||
| if (action != BackgroundHandlerStartDecision.Action.START) { |
There was a problem hiding this comment.
With no handler, nothing says so any more: this returns without a log. On main, each background push without a handler logged A background message could not be handled in Dart as no onBackgroundMessage handler has been registered. from onHandleWork, and that's the line people find when their handler never runs (#8062). Could the receiver log it here, with a different line when the manifest entry turned the handler off? A Log.d would do if a warning on every push is too noisy for apps without a handler.
|
|
||
| Registration is stored on the device. Removing `onBackgroundMessage` from `main()` does not | ||
| clear it, and a push can start the process without running `main()`. Installs that have not | ||
| been opened since the update still start a background Flutter engine. |
There was a problem hiding this comment.
Opening the app doesn't clear it either. Nothing removes the stored handles except the new opt-out, so every install that registered a handler keeps starting the engine on background pushes, opened or not. As written, this reads as if only unopened installs are affected, which could lead someone to skip the flag. Maybe replace the last sentence with "So every install that registered a handler keeps starting a background Flutter engine on pushes, including installs that have not been opened since the update."
…s registered A background push started the messaging service even when the app had never registered onBackgroundMessage, and a stored callback handle was never cleared after that registration was removed. Skip the service unless a handler is stored, and honor a manifest flag so an updated app can turn a leftover handler off on installs that are never opened.
The changelog is written at release, and the service test only checked that stored handles were deleted. A background push with no handler, and a second APK with the manifest opt-out, now assert that the service does not start.
The review flagged three comments that only repeat the null check, the manifest default, and the enabled fallback.
The review asked to keep only the notes the code cannot show: how Android stores the manifest value, and that preference writes stay off the main thread.
The receiver returned without a log when no handler was registered, so the warning people search for never appeared. Also state that every install with a stored handler keeps starting the engine.
dc1be98 to
ffc1ae7
Compare
Description
On Android, a push that arrives while the app is not in the foreground always starts
FlutterFirebaseMessagingBackgroundService, even when the app never registeredFirebaseMessaging.onBackgroundMessage. The service only logs that there is no handler. The registration is also written to disk and never removed, so an app that drops the handler in a later release still starts a background Flutter engine on every push. Installs that have not been opened since that update are included, because a push starts the process without runningmain().This skips the service when no callback handle is stored. A new application meta-data entry,
firebase_messaging_background_handler_enabled, set tofalseskips the service and deletes a leftover handle, including on installs that are never opened. Leaving the entry out, or setting it totrue, keeps today's behavior: a stored handler still runs.falsewins until the entry is removed, and the push that honors it deletes the stored registration, so removing the entry later does not restore the old handler.Related Issues
Fixes #18940
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]).This will ensure a smooth and quick review process. Updating the
pubspec.yamland changelogs is not required.///).melos run analyze) does not report any problems on my PR.Breaking Change
Does your PR require plugin users to manually update their apps to accommodate your change?
Test plan
:firebase_messaging:testDebugUnitTestpasses, including no stored handler, a stored handler, manifestfalseas a boolean and as the string"false", and service startup with the opt-out set.firebase_messaging_background_handler_enabledset tofalseclears stored handles42and99and does not start the service.android-background-handler-disabledjob.