Skip to content

fix(android): stop ActiveCallService when restarted with no calls metadata (WT-1139) - #367

Merged
SERDUN merged 1 commit into
developfrom
fix/active-call-service-empty-restart
Aug 19, 2026
Merged

fix(android): stop ActiveCallService when restarted with no calls metadata (WT-1139)#367
SERDUN merged 1 commit into
developfrom
fix/active-call-service-empty-restart

Conversation

@SERDUN

@SERDUN SERDUN commented Aug 19, 2026

Copy link
Copy Markdown
Member

Overview

Supersedes #346 with the same change rebuilt as a single commit on top of the current develop.

A START_STICKY restart after a process kill delivers a null intent, leaving ActiveCallService with an empty calls list. The NotificationManager static list of active calls lives in the :callkeep_core process and died with it, so nothing ever stops the orphaned instance: its ongoing FGS notification stays in the shade, cannot be swiped away, and the Hang up action has no visible effect (tearDownService tears down the connection services but does not stop this one). The user has to force-stop the app to get rid of the notification. The defect is pinned by the reproduction tests merged in #366; this change flips them to the corrected behavior.

Changes

ActiveCallService:

  • Empty-metadata guard in onStartCommand: after satisfying the startForeground contract, an instance with no calls metadata logs a WARN, tears down the connection services, removes the notification and stops itself, returning START_NOT_STICKY. The teardown matters beyond the notification: if only the main process was killed, a call leg may have survived in :callkeep_core (Telecom keeps that process alive via its own binding), and this restart is the last signal the main process gets about it - without the teardown the device stays stuck in a zombie Telecom call with no UI to end it.
  • Foreground promotion degrades gracefully on Android 14+: promoting with the MICROPHONE type from the background (exactly the sticky-restart case) throws SecurityException; on that exception the promotion retries with the phone-call type only (not while-in-use restricted), mirroring the existing StandaloneCallService fallback, so the guard runs instead of crash-looping the restart.
  • hungUpCall falls back to the CallMetadata carried in the Decline PendingIntent extras: a Hang up tap that creates a fresh service instance (the in-memory list is empty after process death) now hangs up the specific call via the normal SIP BYE flow instead of tearing all connections down. The tear-down branch remains only for the truly unknown case (a notification re-posted by a null-intent restart carries no extras).
  • Both stop branches use stopSelf(startId) (extracted into a tearDownAndStop(startId) helper), so a newer start with real metadata already queued behind the current command (the recovering app re-posting its call list) survives together with its foreground notification.
  • The "metadata" intent extra key is promoted to ActiveCallService.EXTRA_CALLS_METADATA, shared by the writer (NotificationManager.upsertActiveCallsService), the reader and the test.

The normal path deliberately stays START_STICKY: the sticky restart is now the mechanism that delivers the zombie-leg cleanup above, and the guard's START_NOT_STICKY return prevents any restart loop (one restart, one cleanup, done).

Related cleanups:

  • IncomingCallService.performAnswerCall: dropped the misleading START_STICKY return value - the method is called from onConnectionEvent only, so the value never reached onStartCommand (that service deliberately stays START_NOT_STICKY everywhere).
  • background-services.md: the section that described this scenario as an open limitation now describes the guarded behavior, and the Lifecycle / Hang up notes match the code above.

Tests

ActiveCallServiceRestartTest replaces the reproduction version of the file from #366 - the same scenarios now assert the corrected behavior:

  • null-intent restart stops self and removes the notification
  • explicitly empty metadata list stops self
  • start with calls metadata stays foreground and sticky
  • Hang up with no known calls stops self and removes the notification
  • Hang up with metadata only in the intent extras routes the hangup and keeps the service
  • Hang up with a known call keeps the service running (normal path)

Full Android native suite: 253 tests, 0 failures.

…adata (WT-1139)

A system restart of the active-call service after a process kill used to
leave a half-empty ongoing notification that could not be swiped away, with
Hang up having no effect - only force-stopping the app removed it. The
service now detects such an empty restart, cleans up any surviving call leg,
removes the notification and stops itself; a Hang up tapped on a fresh
instance hangs up the specific call it was tapped for instead of tearing
everything down. The services document is updated to describe the guarded
behavior it previously listed as a known limitation.

Verified by the restart tests introduced with the reproduction change, now
asserting the corrected behavior, and the full native suite.
@SERDUN
SERDUN merged commit 3a8bd5e into develop Aug 19, 2026
2 checks passed
@SERDUN
SERDUN deleted the fix/active-call-service-empty-restart branch August 19, 2026 08:46
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