didChangeAppLifecycleState method
Called when the system puts the app in the background or returns the app to the foreground.
An example of implementing this method is provided in the class-level documentation for the WidgetsBindingObserver class.
This method exposes notifications from SystemChannels.lifecycle.
See also:
- AppLifecycleListener, an alternative API for responding to application lifecycle changes.
Implementation
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// The session observes the lifecycle itself, but its observer runs after
// this one. Rotate here first, so a session that timed out ends the waits
// before this return's `app_foregrounded` starts its own.
if (state == AppLifecycleState.resumed) {
_services?.sessionManager.onForeground();
}
if (state == AppLifecycleState.resumed) {
// The trampoline relaunches the host's Activity, so a warm debug-settings
// deeplink arrives here rather than as a route push.
unawaited(_consumeRelayedDebugDeepLink());
}
// Waits keep counting in the background but end only on screen: on the
// return, read the sleep clock, then fire every wait that ended meanwhile.
if (state == AppLifecycleState.resumed) {
unawaited(_sleepClock.sync().then((_) {
if (!_inBackground) _engine.resume();
}));
_dwellTracker.resume();
} else if (state == AppLifecycleState.hidden ||
state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
_engine.pause();
_dwellTracker.pause();
}
// DMC §6.2: a return from the background, not a dialog over the app.
if (state == AppLifecycleState.resumed) {
if (_inBackground) {
_inBackground = false;
// DMC §3.7: a return after the context expired, or after the session
// went idle, fetches it again. A context still valid costs no call.
// Not awaited: this return's signal is checked against the copy held
// now, and the next signal sees the new one.
if (_initialized) unawaited(_refreshUserContext());
if (_appOpenedDue) {
_appOpenedDue = false;
_sendAppOpened();
} else {
_engine.send(const AppForegroundedSignal());
}
}
} else if (state == AppLifecycleState.hidden ||
state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
_inBackground = true;
// Held signals are of the app that was on screen (R58).
if (!_engine.isReady) _engine.dropHeld();
}
// Pause the PiP's player and close its open expanded span while the app is
// away, so `engaged_ms` counts only time the window was actually watchable.
// Without this the span stays open across the whole background stretch.
//
// `inactive` is deliberately excluded: it fires for transient interruptions
// the user is still looking through — the app switcher, an incoming call,
// Control Centre — and pausing playback for those would stutter the window
// on every glance. `hidden` and `paused` are the real "no longer on screen"
// signals, which is where Android's ON_STOP draws the same line.
switch (state) {
case AppLifecycleState.resumed:
_pipOrchestrator.setAppForegrounded(true);
_floaterStoryOrchestrator.setAppForegrounded(true);
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
_coordinator.noteAppLeftForeground();
_pipOrchestrator.setAppForegrounded(false);
_floaterStoryOrchestrator.setAppForegrounded(false);
break;
}
// detached = permanent process destruction. Not paused (every background).
// The owner flushes both queues; neither knows about the other.
_services?.appLifecycleChanged(state);
if (state == AppLifecycleState.detached) {
final plugin = _activePlugin;
if (plugin != null) {
// Same ordering as register(): settle what the plugin owns while its
// bridge is alive, then tear it down.
_coordinator.detach(plugin.id);
plugin.detach();
}
_activePlugin = null;
}
// Closes/reopens the live-test stream on background/resume so a stale
// presence lease isn't held between heartbeats.
_services?.liveTestService.onAppLifecycleChanged(state);
}