G
YouTube TikTok Games
Skip to content

fix(messaging,android): skip the background service when no handler is registered - #18942

Open
SelaseKay wants to merge 5 commits into
mainfrom
fix/messaging-android-background-handler-gate
Open

SelaseKay wants to merge 5 commits into
mainfrom
fix/messaging-android-background-handler-gate

Conversation

@SelaseKay

Copy link
Copy Markdown
Contributor

Description

On Android, a push that arrives while the app is not in the foreground always starts FlutterFirebaseMessagingBackgroundService, even when the app never registered FirebaseMessaging.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 running main().

This skips the service when no callback handle is stored. A new application meta-data entry, firebase_messaging_background_handler_enabled, set to false skips the service and deletes a leftover handle, including on installs that are never opened. Leaving the entry out, or setting it to true, keeps today's behavior: a stored handler still runs. false wins 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.yaml and changelogs is not required.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on my PR.
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Test plan

  • :firebase_messaging:testDebugUnitTest passes, including no stored handler, a stored handler, manifest false as a boolean and as the string "false", and service startup with the opt-out set.
  • On an API 34 emulator, a background push with no stored handler does not start the background service.
  • On an API 34 emulator, an APK with firebase_messaging_background_handler_enabled set to false clears stored handles 42 and 99 and does not start the service.
  • With the gate forced to always start the service, that no-handler emulator test fails because the service runs. The gate was restored after that check.
  • CI, including the new android-background-handler-disabled job.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +136 to +145
Thread {
try {
result.success(
probeBackgroundDelivery(messageId, pluginHandle, userHandle),
)
} catch (e: Exception) {
result.error("PROBE_FAILED", errorMessage(e), null)
}
}
.start()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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()

Comment on lines +99 to +102
} catch (PackageManager.NameNotFoundException exception) {
Log.w(TAG, "Could not read " + METADATA_KEY + " from the manifest.", exception);
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
} 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;
}

Comment on lines +524 to +533
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

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.

Suggested change
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/cloud-messaging/receive.md Outdated

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@SelaseKay
SelaseKay force-pushed the fix/messaging-android-background-handler-gate branch from dc1be98 to ffc1ae7 Compare November 13, 2026 07:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants