Skip to content

Show the Android swap card without a foreground service - #736

Closed
n13 wants to merge 1 commit into
mainfrom
n13/swap-card-without-foreground-service
Closed

n13 wants to merge 1 commit into
mainfrom
n13/swap-card-without-foreground-service

Conversation

@n13

@n13 n13 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

Why

Build 176 is blocked in Play Console by the "Foreground service permissions" declaration. The only thing behind it is the swap card: it was posted by a dataSync foreground service whose job was to keep the process alive so the app could keep polling and redraw the card every 20 seconds. A notification does not need a service to exist or to be updated, so this drops the service instead of declaring it.

What changes

  • The manifest no longer asks for FOREGROUND_SERVICE or FOREGROUND_SERVICE_DATA_SYNC, and SwapLiveActivityService is gone. The merged manifest of the build has no foreground service entry left.
  • SwapLiveNotifications.show posts the card with NotificationManager.notify. The card is still ongoing while the swap is moving, and still becomes a Live Update on Android 16 QPR1 and later.
  • The card is redrawn by whoever is awake: the app while Android keeps it running, Senoti's push on every status change, and the existing alarm that turns it into "updated N ago" two minutes after the last update. That is the same model the iOS Live Activity already uses.
  • The pill on the custom card shows when the swap should be done, "~3:05 PM", instead of "~5 min left". Nothing redraws the card between updates any more, and a count of minutes would sit there going wrong. A time of day stays true.

Senoti needs no change. It already sends these updates as data-only FCM messages with android.priority: HIGH (FcmService::send_data, covered by send_data_is_a_high_priority_data_message_without_a_notification).

Behaviour to expect

  • While the app is in the background and Android has frozen it, the card only moves on a push. With no status change for two minutes it shows the stale layout until the next push or until the app is opened. Before, the service kept it fresh the whole time.
  • Opening the app refreshes the card immediately.

Why not a ticking countdown

I first built the pill as a native Chronometer counting down, which ticks with no process. It cannot stop at zero, and the only wake-up available without a service is an inexact alarm. On the emulator that alarm arrived 21 seconds late and the pill read "~−00:21 left" until it did. In Doze it can be minutes.

Tested

  • melos run format, melos run analyze, the swap live activity Dart tests (58 pass), and :app:testDebugUnitTest (30 pass).
  • Android 17 emulator: the card posts as a promoted Live Update with flags ONGOING_EVENT|ONLY_ALERT_ONCE|PROMOTED_ONGOING, the app has no running service, and the alarm flips it to the stale layout with the app in the background.
  • Android 12 emulator: the custom card posts, a swap_live_activity message delivered to SwapLivePushReceiver while the app is in the background redraws it with the new time, the alarm marks it stale, reopening the app refreshes it, and clearing the swap removes it.

Not tested: a physical device, and a real FCM delivery from Senoti (the push was a broadcast to the same receiver).

After this ships

Play Console keeps asking for the declaration while any active release still carries the permission, so the answer for build 176 is still "yes, data sync". Once a build with this change replaces it on every track, the declaration can be switched to "no".

The card was posted by a dataSync foreground service that held the process
so status polls and a 20 second redraw kept running. Google Play asks for a
declaration and a demo video for that permission. The card is now a plain
notification: the running app and Senoti's pushes redraw it, and the
existing alarm marks it stale. The manifest no longer asks for
FOREGROUND_SERVICE or FOREGROUND_SERVICE_DATA_SYNC.

With nothing redrawing the card between updates, a count of the minutes
left would go wrong, so the card shows the time the swap should be done by.
@n13 n13 added the bot-review Request automated review from review-bot label Nov 11, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict: Approve
Reviewer: GPT-6.1 Sol

Reviewed c819e4186be18d408316b222d90aad07bfd26f9a against base 53f57a96ecd1e154a3b20251de1d2d43100bcc91. No blocking findings.

The direct notification write preserves the existing revision/dismissal checks, stale alarm, ongoing state, completion updates, and explicit cancellation. Both the native FCM receiver and Dart push handler reach this path; no references to the removed foreground service remain. The absolute ETA avoids a frozen countdown, and the payload contract stays compatible with Senoti and iOS. Background freshness depending on pushes is the intended behavior change.

The retained promotion settings match Android's Live Update requirements. The native receiver renders directly from the push payload, consistent with FCM's processing guidance.

Validation:

  • git diff --check: passed. Read-only Melos formatting: zero changed files, with unresolved dependency warnings.
  • GitHub CI passed at this exact head, including formatting, analysis, and mobile, SDK, and cold-wallet tests.
  • Local locked bootstrap was blocked by installed Flutter 3.44.4 versus pinned 3.47.7; focused Dart tests could not start without resolved dependencies, and workspace analysis was stopped at the 10-second cap. Android unit tests could not start because this clean checkout lacks gradlew, and no Java runtime is installed. No tracked files were changed.

Device behavior and real FCM delivery were not independently exercised in this review.

@n13 n13 removed the bot-review Request automated review from review-bot label Nov 11, 2026
@n13

n13 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Closing without merging. Without the service nothing of ours runs between pushes, so the card cannot advance its own countdown or status, and it goes stale after two minutes in the background. We are keeping the foreground service that shipped in build 176 and filing the Play Console foreground service declaration for it instead. The branch stays in case we come back to this.

@n13 n13 closed this Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant