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>
This commit is contained in:
François 2023-10-23 22:47:55 +02:00 committed by GitHub
parent 51c70bc98c
commit 7d504b89c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 23 deletions

View File

@ -330,3 +330,22 @@ pub struct WindowThemeChanged {
/// The new system theme.
pub theme: WindowTheme,
}
/// Application lifetime events
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
#[reflect(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum ApplicationLifetime {
/// The application just started.
Started,
/// The application was suspended.
///
/// On Android, applications have one frame to react to this event before being paused in the background.
Suspended,
/// The application was resumed.
Resumed,
}

View File

@ -98,7 +98,8 @@ impl Plugin for WindowPlugin {
.add_event::<WindowBackendScaleFactorChanged>()
.add_event::<FileDragAndDrop>()
.add_event::<WindowMoved>()
.add_event::<WindowThemeChanged>();
.add_event::<WindowThemeChanged>()
.add_event::<ApplicationLifetime>();
if let Some(primary_window) = &self.primary_window {
let initial_focus = app
@ -141,7 +142,8 @@ impl Plugin for WindowPlugin {
.register_type::<WindowBackendScaleFactorChanged>()
.register_type::<FileDragAndDrop>()
.register_type::<WindowMoved>()
.register_type::<WindowThemeChanged>();
.register_type::<WindowThemeChanged>()
.register_type::<ApplicationLifetime>();
// Register window descriptor and related types
app.register_type::<Window>()

View File

@ -38,10 +38,10 @@ use bevy_utils::{
Duration, Instant,
};
use bevy_window::{
exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged,
WindowCloseRequested, WindowCreated, WindowDestroyed, WindowFocused, WindowMoved,
WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged,
};
#[cfg(target_os = "android")]
use bevy_window::{PrimaryWindow, RawHandleWrapper};
@ -279,6 +279,7 @@ struct WindowAndInputEventWriters<'w> {
window_moved: EventWriter<'w, WindowMoved>,
window_theme_changed: EventWriter<'w, WindowThemeChanged>,
window_destroyed: EventWriter<'w, WindowDestroyed>,
lifetime: EventWriter<'w, ApplicationLifetime>,
keyboard_input: EventWriter<'w, KeyboardInput>,
character_input: EventWriter<'w, ReceivedCharacter>,
mouse_button_input: EventWriter<'w, MouseButtonInput>,
@ -298,8 +299,8 @@ struct WindowAndInputEventWriters<'w> {
/// Persistent state that is used to run the [`App`] according to the current
/// [`UpdateMode`].
struct WinitAppRunnerState {
/// Is `true` if the app is running and not suspended.
is_active: bool,
/// Current active state of the app.
active: ActiveState,
/// Is `true` if a new [`WindowEvent`] has been received since the last update.
window_event_received: bool,
/// Is `true` if the app has requested a redraw since the last update.
@ -312,10 +313,28 @@ struct WinitAppRunnerState {
scheduled_update: Option<Instant>,
}
#[derive(PartialEq, Eq)]
enum ActiveState {
NotYetStarted,
Active,
Suspended,
WillSuspend,
}
impl ActiveState {
#[inline]
fn should_run(&self) -> bool {
match self {
ActiveState::NotYetStarted | ActiveState::Suspended => false,
ActiveState::Active | ActiveState::WillSuspend => true,
}
}
}
impl Default for WinitAppRunnerState {
fn default() -> Self {
Self {
is_active: false,
active: ActiveState::NotYetStarted,
window_event_received: false,
redraw_requested: false,
wait_elapsed: false,
@ -700,19 +719,23 @@ pub fn winit_runner(mut app: App) {
});
}
event::Event::Suspended => {
runner_state.is_active = false;
#[cfg(target_os = "android")]
{
// Remove the `RawHandleWrapper` from the primary window.
// This will trigger the surface destruction.
let mut query = app.world.query_filtered::<Entity, With<PrimaryWindow>>();
let entity = query.single(&app.world);
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
*control_flow = ControlFlow::Wait;
}
let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
event_writers.lifetime.send(ApplicationLifetime::Suspended);
// Mark the state as `WillSuspend`. This will let the schedule run one last time
// before actually suspending to let the application react
runner_state.active = ActiveState::WillSuspend;
}
event::Event::Resumed => {
runner_state.is_active = true;
let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
match runner_state.active {
ActiveState::NotYetStarted => {
event_writers.lifetime.send(ApplicationLifetime::Started);
}
_ => {
event_writers.lifetime.send(ApplicationLifetime::Resumed);
}
}
runner_state.active = ActiveState::Active;
#[cfg(target_os = "android")]
{
// 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) {
}
}
event::Event::MainEventsCleared => {
if runner_state.is_active {
if runner_state.active.should_run() {
if runner_state.active == ActiveState::WillSuspend {
runner_state.active = ActiveState::Suspended;
#[cfg(target_os = "android")]
{
// Remove the `RawHandleWrapper` from the primary window.
// This will trigger the surface destruction.
let mut query =
app.world.query_filtered::<Entity, With<PrimaryWindow>>();
let entity = query.single(&app.world);
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
*control_flow = ControlFlow::Wait;
}
}
let (config, windows) = focused_windows_state.get(&app.world);
let focused = windows.iter().any(|window| window.focused);
let should_update = match config.update_mode(focused) {

View File

@ -2,7 +2,11 @@
// type aliases tends to obfuscate code while offering no improvement in code cleanliness.
#![allow(clippy::type_complexity)]
use bevy::{input::touch::TouchPhase, prelude::*, window::WindowMode};
use bevy::{
input::touch::TouchPhase,
prelude::*,
window::{ApplicationLifetime, WindowMode},
};
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
#[bevy_main]
@ -17,7 +21,7 @@ fn main() {
..default()
}))
.add_systems(Startup, (setup_scene, setup_music))
.add_systems(Update, (touch_camera, button_handler));
.add_systems(Update, (touch_camera, button_handler, handle_lifetime));
// MSAA makes some Android devices panic, this is under investigation
// https://github.com/bevyengine/bevy/issues/8229
@ -161,3 +165,18 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
settings: PlaybackSettings::LOOP,
});
}
// Pause audio when app goes into background and resume when it returns.
// This is handled by the OS on iOS, but not on Android.
fn handle_lifetime(
mut lifetime_events: EventReader<ApplicationLifetime>,
music_controller: Query<&AudioSink>,
) {
for event in lifetime_events.read() {
match event {
ApplicationLifetime::Suspended => music_controller.single().pause(),
ApplicationLifetime::Resumed => music_controller.single().play(),
ApplicationLifetime::Started => (),
}
}
}