Skip to content

Commit 7d504b8

Browse files
mockersfcoreh
andauthored
Application lifetime events (suspend audio on Android) (#10158)
# Objective - Handle pausing audio when Android app is suspended ## Solution - This is the start of application lifetime events. They are mostly useful on mobile - Next version of winit should add a few more - When application is suspended, send an event to notify the application, and run the schedule one last time before actually suspending the app - Audio is now suspended too 🎉 https://github.com/bevyengine/bevy/assets/8672791/d74e2e09-ee29-4f40-adf2-36a0c064f94e --------- Co-authored-by: Marco Buono <418473+coreh@users.noreply.github.com>
1 parent 51c70bc commit 7d504b8

File tree

4 files changed

+99
-23
lines changed

4 files changed

+99
-23
lines changed

crates/bevy_window/src/event.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,22 @@ pub struct WindowThemeChanged {
330330
/// The new system theme.
331331
pub theme: WindowTheme,
332332
}
333+
334+
/// Application lifetime events
335+
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
336+
#[reflect(Debug, PartialEq)]
337+
#[cfg_attr(
338+
feature = "serialize",
339+
derive(serde::Serialize, serde::Deserialize),
340+
reflect(Serialize, Deserialize)
341+
)]
342+
pub enum ApplicationLifetime {
343+
/// The application just started.
344+
Started,
345+
/// The application was suspended.
346+
///
347+
/// On Android, applications have one frame to react to this event before being paused in the background.
348+
Suspended,
349+
/// The application was resumed.
350+
Resumed,
351+
}

crates/bevy_window/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ impl Plugin for WindowPlugin {
9898
.add_event::<WindowBackendScaleFactorChanged>()
9999
.add_event::<FileDragAndDrop>()
100100
.add_event::<WindowMoved>()
101-
.add_event::<WindowThemeChanged>();
101+
.add_event::<WindowThemeChanged>()
102+
.add_event::<ApplicationLifetime>();
102103

103104
if let Some(primary_window) = &self.primary_window {
104105
let initial_focus = app
@@ -141,7 +142,8 @@ impl Plugin for WindowPlugin {
141142
.register_type::<WindowBackendScaleFactorChanged>()
142143
.register_type::<FileDragAndDrop>()
143144
.register_type::<WindowMoved>()
144-
.register_type::<WindowThemeChanged>();
145+
.register_type::<WindowThemeChanged>()
146+
.register_type::<ApplicationLifetime>();
145147

146148
// Register window descriptor and related types
147149
app.register_type::<Window>()

crates/bevy_winit/src/lib.rs

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ use bevy_utils::{
3838
Duration, Instant,
3939
};
4040
use bevy_window::{
41-
exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
42-
ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
43-
WindowCloseRequested, WindowCreated, WindowDestroyed, WindowFocused, WindowMoved,
44-
WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
41+
exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
42+
FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
43+
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
44+
WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
4545
};
4646
#[cfg(target_os = "android")]
4747
use bevy_window::{PrimaryWindow, RawHandleWrapper};
@@ -279,6 +279,7 @@ struct WindowAndInputEventWriters<'w> {
279279
window_moved: EventWriter<'w, WindowMoved>,
280280
window_theme_changed: EventWriter<'w, WindowThemeChanged>,
281281
window_destroyed: EventWriter<'w, WindowDestroyed>,
282+
lifetime: EventWriter<'w, ApplicationLifetime>,
282283
keyboard_input: EventWriter<'w, KeyboardInput>,
283284
character_input: EventWriter<'w, ReceivedCharacter>,
284285
mouse_button_input: EventWriter<'w, MouseButtonInput>,
@@ -298,8 +299,8 @@ struct WindowAndInputEventWriters<'w> {
298299
/// Persistent state that is used to run the [`App`] according to the current
299300
/// [`UpdateMode`].
300301
struct WinitAppRunnerState {
301-
/// Is `true` if the app is running and not suspended.
302-
is_active: bool,
302+
/// Current active state of the app.
303+
active: ActiveState,
303304
/// Is `true` if a new [`WindowEvent`] has been received since the last update.
304305
window_event_received: bool,
305306
/// Is `true` if the app has requested a redraw since the last update.
@@ -312,10 +313,28 @@ struct WinitAppRunnerState {
312313
scheduled_update: Option<Instant>,
313314
}
314315

316+
#[derive(PartialEq, Eq)]
317+
enum ActiveState {
318+
NotYetStarted,
319+
Active,
320+
Suspended,
321+
WillSuspend,
322+
}
323+
324+
impl ActiveState {
325+
#[inline]
326+
fn should_run(&self) -> bool {
327+
match self {
328+
ActiveState::NotYetStarted | ActiveState::Suspended => false,
329+
ActiveState::Active | ActiveState::WillSuspend => true,
330+
}
331+
}
332+
}
333+
315334
impl Default for WinitAppRunnerState {
316335
fn default() -> Self {
317336
Self {
318-
is_active: false,
337+
active: ActiveState::NotYetStarted,
319338
window_event_received: false,
320339
redraw_requested: false,
321340
wait_elapsed: false,
@@ -700,19 +719,23 @@ pub fn winit_runner(mut app: App) {
700719
});
701720
}
702721
event::Event::Suspended => {
703-
runner_state.is_active = false;
704-
#[cfg(target_os = "android")]
705-
{
706-
// Remove the `RawHandleWrapper` from the primary window.
707-
// This will trigger the surface destruction.
708-
let mut query = app.world.query_filtered::<Entity, With<PrimaryWindow>>();
709-
let entity = query.single(&app.world);
710-
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
711-
*control_flow = ControlFlow::Wait;
712-
}
722+
let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
723+
event_writers.lifetime.send(ApplicationLifetime::Suspended);
724+
// Mark the state as `WillSuspend`. This will let the schedule run one last time
725+
// before actually suspending to let the application react
726+
runner_state.active = ActiveState::WillSuspend;
713727
}
714728
event::Event::Resumed => {
715-
runner_state.is_active = true;
729+
let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
730+
match runner_state.active {
731+
ActiveState::NotYetStarted => {
732+
event_writers.lifetime.send(ApplicationLifetime::Started);
733+
}
734+
_ => {
735+
event_writers.lifetime.send(ApplicationLifetime::Resumed);
736+
}
737+
}
738+
runner_state.active = ActiveState::Active;
716739
#[cfg(target_os = "android")]
717740
{
718741
// Get windows that are cached but without raw handles. Those window were already created, but got their
@@ -754,7 +777,20 @@ pub fn winit_runner(mut app: App) {
754777
}
755778
}
756779
event::Event::MainEventsCleared => {
757-
if runner_state.is_active {
780+
if runner_state.active.should_run() {
781+
if runner_state.active == ActiveState::WillSuspend {
782+
runner_state.active = ActiveState::Suspended;
783+
#[cfg(target_os = "android")]
784+
{
785+
// Remove the `RawHandleWrapper` from the primary window.
786+
// This will trigger the surface destruction.
787+
let mut query =
788+
app.world.query_filtered::<Entity, With<PrimaryWindow>>();
789+
let entity = query.single(&app.world);
790+
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
791+
*control_flow = ControlFlow::Wait;
792+
}
793+
}
758794
let (config, windows) = focused_windows_state.get(&app.world);
759795
let focused = windows.iter().any(|window| window.focused);
760796
let should_update = match config.update_mode(focused) {

examples/mobile/src/lib.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// type aliases tends to obfuscate code while offering no improvement in code cleanliness.
33
#![allow(clippy::type_complexity)]
44

5-
use bevy::{input::touch::TouchPhase, prelude::*, window::WindowMode};
5+
use bevy::{
6+
input::touch::TouchPhase,
7+
prelude::*,
8+
window::{ApplicationLifetime, WindowMode},
9+
};
610

711
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
812
#[bevy_main]
@@ -17,7 +21,7 @@ fn main() {
1721
..default()
1822
}))
1923
.add_systems(Startup, (setup_scene, setup_music))
20-
.add_systems(Update, (touch_camera, button_handler));
24+
.add_systems(Update, (touch_camera, button_handler, handle_lifetime));
2125

2226
// MSAA makes some Android devices panic, this is under investigation
2327
// https://github.com/bevyengine/bevy/issues/8229
@@ -161,3 +165,18 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
161165
settings: PlaybackSettings::LOOP,
162166
});
163167
}
168+
169+
// Pause audio when app goes into background and resume when it returns.
170+
// This is handled by the OS on iOS, but not on Android.
171+
fn handle_lifetime(
172+
mut lifetime_events: EventReader<ApplicationLifetime>,
173+
music_controller: Query<&AudioSink>,
174+
) {
175+
for event in lifetime_events.read() {
176+
match event {
177+
ApplicationLifetime::Suspended => music_controller.single().pause(),
178+
ApplicationLifetime::Resumed => music_controller.single().play(),
179+
ApplicationLifetime::Started => (),
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)