Skip to main content

inset_services/
binding.rs

1//! Flutter counterpart: `services/binding.dart` (`ServicesBinding`).
2//!
3//! Only the app life cycle, the application exit and the clipboard's changes are here: the
4//! lifecycle messages the host sends, which reach
5//! `SchedulerBinding::handle_app_lifecycle_state_changed`, `handleRequestAppExit`, which the
6//! widgets binding overrides, `exitApplication`, and the host's word that the clipboard changed,
7//! which Flutter's engines do not send. The keyboard and the restoration manager are singletons
8//! of their own (`KeyEventManager`, `RestorationManager`); the rest waits with platform channels.
9
10use std::rc::Rc;
11
12use inset_embedder::{AppExitResponse, AppExitType, AppLifecycleState};
13use inset_foundation::{App, ChangeNotifierData, Handle, Task};
14use inset_scheduler::SchedulerBinding;
15
16use crate::SystemChrome;
17
18/// The object side of Flutter's `WidgetsBinding` override of
19/// `ServicesBinding.handleRequestAppExit`, which asks the widget layer's observers. Implement it
20/// on the binding above and register `Rc::new(handle)` with [`ServicesBinding::set_overrides`];
21/// `Handle<T>` is then [`ServicesBindingOverrides`].
22pub trait ServicesBindingOverridesObject: Sized + 'static {
23    /// Answers in place of [`ServicesBinding::handle_request_app_exit`]'s own answer.
24    fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse>;
25}
26
27/// What the binding above this one overrides; kept erased.
28pub trait ServicesBindingOverrides {
29    /// See [`ServicesBindingOverridesObject::handle_request_app_exit`].
30    fn handle_request_app_exit(&self, app: &mut App) -> Task<AppExitResponse>;
31}
32
33impl<T: ServicesBindingOverridesObject> ServicesBindingOverrides for Handle<T> {
34    fn handle_request_app_exit(&self, app: &mut App) -> Task<AppExitResponse> {
35        T::handle_request_app_exit(*self, app)
36    }
37}
38
39/// Hands the host's lifecycle messages to the scheduler and the layers above, answers the host's
40/// requests to exit the application, and asks the host to exit it.
41#[derive(Default)]
42pub struct ServicesBinding {
43    overrides: Option<Rc<dyn ServicesBindingOverrides>>,
44    /// Created with the binding so that what shows the clipboard can listen.
45    clipboard: Option<Handle<ChangeNotifierData>>,
46    /// Whether `initInstances`'s work is done, which the first `instance` does.
47    initialized: bool,
48}
49
50impl ServicesBinding {
51    /// The current [`ServicesBinding`], created on first use.
52    ///
53    /// Creating it reads the lifecycle state the host started in, as Dart's `initInstances`
54    /// does.
55    pub fn instance(app: &mut App) -> Handle<ServicesBinding> {
56        let this: Handle<ServicesBinding> = app.singleton();
57        if !app.get(this).initialized {
58            app.get_mut(this).initialized = true;
59            let clipboard = app.create(ChangeNotifierData::new());
60            app.get_mut(this).clipboard = Some(clipboard);
61            this.read_initial_lifecycle_state_from_native_window(app);
62        }
63        this
64    }
65
66    /// Listenable that notifies when another app has changed what is on the clipboard, or a copy
67    /// of this app's expired and was cleared, as far as the host can tell
68    /// (`EmbedderClient::clipboard_changed`). The app's own writes are not told, but where the
69    /// host's system does not tell them apart, as a browser's does not.
70    ///
71    /// What shows whether the clipboard can be pasted, such as a toolbar's Paste button, should
72    /// listen to this and look again.
73    pub fn clipboard(self: Handle<Self>, app: &App) -> Handle<ChangeNotifierData> {
74        app.get(self)
75            .clipboard
76            .expect("ServicesBinding::instance creates the clipboard listenable")
77    }
78
79    /// Notifies [`clipboard`](Self::clipboard): the host says what is on the clipboard changed.
80    pub fn handle_clipboard_changed(self: Handle<Self>, app: &mut App) {
81        self.clipboard(app).notify_listeners(app);
82    }
83
84    /// Registers the binding layered above this one, as Flutter's `WidgetsBinding` does by
85    /// overriding `handleRequestAppExit`.
86    pub fn set_overrides(
87        self: Handle<Self>,
88        app: &mut App,
89        overrides: Rc<dyn ServicesBindingOverrides>,
90    ) {
91        app.get_mut(self).overrides = Some(overrides);
92    }
93
94    // App life cycle
95
96    /// Initializes the [`SchedulerBinding::lifecycle_state`] with the
97    /// `Platform::initial_lifecycle_state`.
98    ///
99    /// Once the lifecycle state is populated through any means (including this method), this
100    /// method will do nothing. This is because the `Platform::initial_lifecycle_state` may
101    /// already be stale and it no longer makes sense to use the initial state at startup as the
102    /// current state anymore.
103    ///
104    /// The latest state should be obtained by subscribing to
105    /// `WidgetsBindingObserver::did_change_app_lifecycle_state`.
106    pub fn read_initial_lifecycle_state_from_native_window(self: Handle<Self>, app: &mut App) {
107        let initial_lifecycle_state = app.platform().initial_lifecycle_state();
108        let Some(initial_lifecycle_state) = initial_lifecycle_state else {
109            return;
110        };
111        if SchedulerBinding::lifecycle_state(app).is_some() {
112            return;
113        }
114        self.handle_lifecycle_message(app, initial_lifecycle_state);
115    }
116
117    /// Handles a lifecycle state the host reports: Dart's `_handleLifecycleMessage`, which
118    /// `SystemChannels.lifecycle` calls with the state's name, and which the host reaches through
119    /// `EmbedderClient::app_lifecycle_state_changed`. The states between the one the app is in
120    /// and `state` are handled first, in order.
121    pub fn handle_lifecycle_message(self: Handle<Self>, app: &mut App, state: AppLifecycleState) {
122        let generated = generate_state_transitions(SchedulerBinding::lifecycle_state(app), state);
123        for state_change in generated {
124            SchedulerBinding::handle_app_lifecycle_state_changed(app, state_change);
125            SystemChrome::handle_app_lifecycle_state_changed(app, state_change);
126        }
127    }
128
129    /// Handles any requests for application exit that the host sends through
130    /// `EmbedderClient::request_app_exit`.
131    ///
132    /// By default, returns [`AppExitResponse::Exit`].
133    ///
134    /// Not all exits are cancelable, so not all exits will call this function. Do not rely on
135    /// this function as a place to save critical data, because you will be disappointed. There
136    /// are a number of ways that the application can exit without letting the application know
137    /// first: power can be unplugged, the battery removed, the application can be killed in a
138    /// task manager or command line, or the device could have a rapid unplanned disassembly
139    /// (i.e. it could explode). In all of those cases (and probably others), no notification
140    /// will be given to the application that it is about to exit.
141    ///
142    /// See also:
143    ///
144    /// * `WidgetsBindingObserver::did_request_app_exit`, which can be overridden to respond to
145    ///   this message.
146    /// * `WidgetsBinding`'s override of this method, which notifies its observers.
147    pub fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
148        let overrides = app.get(self).overrides.clone();
149        match overrides {
150            Some(overrides) => overrides.handle_request_app_exit(app),
151            None => Task::ready(AppExitResponse::Exit),
152        }
153    }
154
155    /// Exits the application by calling the native application API method for exiting an
156    /// application cleanly.
157    ///
158    /// This differs from calling [`std::process::exit`] in that it gives the engine a chance
159    /// to clean up resources so that it doesn't crash on exit, so calling this is always
160    /// preferred over calling [`std::process::exit`]. It also optionally gives handlers of
161    /// [`handle_request_app_exit`](Self::handle_request_app_exit) a chance to cancel the
162    /// application exit.
163    ///
164    /// The `exit_type` indicates what kind of exit to perform. For
165    /// [`AppExitType::Cancelable`] exits, the application is queried through a call to
166    /// [`handle_request_app_exit`](Self::handle_request_app_exit), where the application can
167    /// optionally cancel the request for exit. If the `exit_type` is
168    /// [`AppExitType::Required`], then the application exits immediately without querying the
169    /// application.
170    ///
171    /// For [`AppExitType::Cancelable`] exits, the returned response value is the response
172    /// obtained from the application as to whether the exit was canceled or not. Practically,
173    /// the response will never be [`AppExitResponse::Exit`], since the application will have
174    /// already exited by the time the result would have been received.
175    ///
176    /// The `exit_code` argument will be used as the application exit code on platforms where
177    /// an exit code is supported. On other platforms it may be ignored. Dart's default is
178    /// zero.
179    ///
180    /// Flutter's engine asks the framework back for a cancelable exit. Here the framework asks
181    /// itself, on a later turn as the engine's request would come, and only a decided exit
182    /// reaches the host, as [`AppExit::quit`](inset_embedder::AppExit::quit); a host that cannot
183    /// quit cancels every exit.
184    ///
185    /// See also:
186    ///
187    /// * `WidgetsBindingObserver::did_request_app_exit` for a handler you can override on a
188    ///   `WidgetsBindingObserver` to receive exit requests.
189    pub fn exit_application(
190        self: Handle<Self>,
191        app: &App,
192        exit_type: AppExitType,
193        exit_code: i32,
194    ) -> Task<AppExitResponse> {
195        let platform = app.platform();
196        let Some(app_exit) = platform.app_exit() else {
197            return Task::ready(AppExitResponse::Cancel);
198        };
199        match exit_type {
200            AppExitType::Required => {
201                app_exit.quit(exit_code);
202                Task::ready(AppExitResponse::Exit)
203            }
204            AppExitType::Cancelable => app.spawn(async move |cx| {
205                let response = cx.update(|app| self.handle_request_app_exit(app)).await;
206                if response == AppExitResponse::Exit {
207                    cx.update(|app| quit(app, exit_code));
208                }
209                response
210            }),
211        }
212    }
213}
214
215/// Quits through the host, where it can quit.
216fn quit(app: &App, exit_code: i32) {
217    if let Some(app_exit) = app.platform().app_exit() {
218        app_exit.quit(exit_code);
219    }
220}
221
222/// The states from `previous_state` to `state`, in the order the state machine goes through
223/// them, `state` last; `state` alone from no state, and none to the state already held.
224fn generate_state_transitions(
225    previous_state: Option<AppLifecycleState>,
226    state: AppLifecycleState,
227) -> Vec<AppLifecycleState> {
228    if previous_state == Some(state) {
229        return Vec::new();
230    }
231    let mut state_changes = Vec::new();
232    match previous_state {
233        // If there was no previous state, just jump directly to the new state.
234        None => state_changes.push(state),
235        Some(previous_state) => {
236            let previous_state_index = state_index(previous_state);
237            let state_index = state_index(state);
238            if state == AppLifecycleState::Detached {
239                state_changes.extend(STATE_ORDER[previous_state_index + 1..].iter().copied());
240                state_changes.push(AppLifecycleState::Detached);
241            } else if previous_state_index > state_index {
242                state_changes.extend(STATE_ORDER[state_index..previous_state_index].iter().rev());
243            } else {
244                state_changes.extend(&STATE_ORDER[previous_state_index + 1..=state_index]);
245            }
246        }
247    }
248    debug_assert!(
249        {
250            let mut starting = previous_state;
251            state_changes.iter().all(|&ending| {
252                let valid = debug_verify_lifecycle_change(starting, ending);
253                starting = Some(ending);
254                valid
255            })
256        },
257        "Invalid lifecycle state transition generated from {previous_state:?} to {state:?} (generated {state_changes:?})"
258    );
259    state_changes
260}
261
262/// Dart's `AppLifecycleState.values`: the order the state machine goes through.
263const STATE_ORDER: [AppLifecycleState; 5] = [
264    AppLifecycleState::Detached,
265    AppLifecycleState::Resumed,
266    AppLifecycleState::Inactive,
267    AppLifecycleState::Hidden,
268    AppLifecycleState::Paused,
269];
270
271fn state_index(state: AppLifecycleState) -> usize {
272    STATE_ORDER
273        .iter()
274        .position(|&ordered| ordered == state)
275        .expect("every state is in the order")
276}
277
278fn debug_verify_lifecycle_change(
279    starting: Option<AppLifecycleState>,
280    ending: AppLifecycleState,
281) -> bool {
282    let Some(starting) = starting else {
283        // Any transition from null is fine, since it is initializing the state.
284        return true;
285    };
286    if starting == ending {
287        // Any transition to itself shouldn't happen.
288        return false;
289    }
290    match starting {
291        // Can't go from resumed to detached directly (must go through paused).
292        AppLifecycleState::Resumed => ending == AppLifecycleState::Inactive,
293        AppLifecycleState::Detached => {
294            ending == AppLifecycleState::Resumed || ending == AppLifecycleState::Paused
295        }
296        AppLifecycleState::Inactive => {
297            ending == AppLifecycleState::Resumed || ending == AppLifecycleState::Hidden
298        }
299        AppLifecycleState::Hidden => {
300            ending == AppLifecycleState::Paused || ending == AppLifecycleState::Inactive
301        }
302        AppLifecycleState::Paused => {
303            ending == AppLifecycleState::Hidden || ending == AppLifecycleState::Detached
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use std::cell::Cell;
311    use std::rc::Rc;
312
313    use std::future::Future;
314
315    use inset_embedder::AppExit;
316    use inset_foundation::{AppCell, Completer};
317    use inset_test::TestPlatform;
318
319    use super::*;
320
321    /// A host that records the exit code it was told to quit with.
322    #[derive(Default)]
323    struct RecordingAppExit {
324        quit_with: Cell<Option<i32>>,
325    }
326
327    impl AppExit for RecordingAppExit {
328        fn quit(&self, exit_code: i32) {
329            self.quit_with.set(Some(exit_code));
330        }
331    }
332
333    /// An app whose host quits through `host`.
334    fn quitting_through(host: &Rc<RecordingAppExit>) -> Rc<AppCell> {
335        AppCell::with_platform(Rc::new(
336            TestPlatform::new().with_app_exit(Rc::clone(host) as Rc<dyn AppExit>),
337        ))
338    }
339
340    /// Makes `binding` the one above the services binding's own answer.
341    fn answer_with<T: ServicesBindingOverridesObject + 'static>(cell: &Rc<AppCell>, binding: T) {
342        let mut app = cell.borrow_mut();
343        let binding = app.create(binding);
344        ServicesBinding::instance(&mut app).set_overrides(&mut app, Rc::new(binding));
345    }
346
347    /// Awaits `answer` in a task, and runs the checkpoint that task needs; `None` while the
348    /// answer is still on its way.
349    fn awaited(
350        cell: &Rc<AppCell>,
351        answer: impl Future<Output = AppExitResponse> + 'static,
352    ) -> Rc<Cell<Option<AppExitResponse>>> {
353        let awaited = Rc::new(Cell::new(None));
354        let out = Rc::clone(&awaited);
355        cell.borrow()
356            .spawn(async move |_cx| out.set(Some(answer.await)));
357        cell.checkpoint();
358        awaited
359    }
360
361    fn exit_application(cell: &Rc<AppCell>, exit_type: AppExitType) -> AppExitResponse {
362        let response = {
363            let mut app = cell.borrow_mut();
364            ServicesBinding::instance(&mut app).exit_application(&app, exit_type, 0)
365        };
366        let answer = awaited(cell, response).get();
367        answer.expect("the exit was answered")
368    }
369
370    fn handle_request_app_exit(cell: &Rc<AppCell>) -> Rc<Cell<Option<AppExitResponse>>> {
371        let response = {
372            let mut app = cell.borrow_mut();
373            ServicesBinding::instance(&mut app).handle_request_app_exit(&mut app)
374        };
375        awaited(cell, response)
376    }
377
378    // widgets/binding_test.dart 'handleLifecycleStateChanged callback'.
379    #[test]
380    fn the_states_between_are_generated_in_order() {
381        use AppLifecycleState::*;
382        let steps = [
383            (None, Paused, vec![Paused]),
384            (Some(Paused), Resumed, vec![Hidden, Inactive, Resumed]),
385            (Some(Resumed), Paused, vec![Inactive, Hidden, Paused]),
386            (Some(Paused), Inactive, vec![Hidden, Inactive]),
387            (Some(Inactive), Hidden, vec![Hidden]),
388            (Some(Hidden), Paused, vec![Paused]),
389            (Some(Paused), Detached, vec![Detached]),
390            (Some(Detached), Resumed, vec![Resumed]),
391            (
392                Some(Resumed),
393                Detached,
394                vec![Inactive, Hidden, Paused, Detached],
395            ),
396            (Some(Hidden), Hidden, vec![]),
397        ];
398        for (previous_state, state, generated) in steps {
399            assert_eq!(
400                generate_state_transitions(previous_state, state),
401                generated,
402                "from {previous_state:?} to {state:?}"
403            );
404        }
405    }
406
407    #[test]
408    fn the_initial_state_is_read_when_the_binding_is_made() {
409        let cell = AppCell::with_platform(Rc::new(
410            TestPlatform::new().with_initial_lifecycle_state(AppLifecycleState::Hidden),
411        ));
412        let mut app = cell.borrow_mut();
413        ServicesBinding::instance(&mut app);
414        assert_eq!(
415            SchedulerBinding::lifecycle_state(&mut app),
416            Some(AppLifecycleState::Hidden)
417        );
418        assert!(!SchedulerBinding::frames_enabled(&mut app));
419    }
420
421    #[test]
422    fn a_host_without_an_initial_state_leaves_the_app_without_one() {
423        let cell = AppCell::new();
424        let mut app = cell.borrow_mut();
425        ServicesBinding::instance(&mut app);
426        assert_eq!(SchedulerBinding::lifecycle_state(&mut app), None);
427    }
428
429    #[test]
430    fn a_required_exit_quits_without_asking() {
431        let host = Rc::default();
432        let cell = quitting_through(&host);
433        answer_with(&cell, Refusing);
434        let response = exit_application(&cell, AppExitType::Required);
435        assert_eq!(host.quit_with.get(), Some(0));
436        assert_eq!(response, AppExitResponse::Exit);
437    }
438
439    #[test]
440    fn a_cancelable_exit_quits_once_the_app_agrees() {
441        let host = Rc::default();
442        let cell = quitting_through(&host);
443        let response = exit_application(&cell, AppExitType::Cancelable);
444        assert_eq!(host.quit_with.get(), Some(0));
445        assert_eq!(response, AppExitResponse::Exit);
446    }
447
448    #[test]
449    fn a_cancelable_exit_the_app_refuses_does_not_quit() {
450        let host = Rc::default();
451        let cell = quitting_through(&host);
452        answer_with(&cell, Refusing);
453        let response = exit_application(&cell, AppExitType::Cancelable);
454        assert_eq!(host.quit_with.get(), None);
455        assert_eq!(response, AppExitResponse::Cancel);
456    }
457
458    #[test]
459    fn exit_application_is_canceled_where_the_host_cannot_exit() {
460        let cell = AppCell::with_platform(Rc::new(TestPlatform::new()));
461        let response = exit_application(&cell, AppExitType::Cancelable);
462        assert_eq!(response, AppExitResponse::Cancel);
463    }
464
465    #[test]
466    fn default_handle_request_app_exit_returns_exit() {
467        let cell = AppCell::new();
468        let response = handle_request_app_exit(&cell);
469        assert_eq!(response.get(), Some(AppExitResponse::Exit));
470    }
471
472    /// Refuses every exit, as a binding whose observer cancels.
473    struct Refusing;
474
475    impl ServicesBindingOverridesObject for Refusing {
476        fn handle_request_app_exit(self: Handle<Self>, _app: &mut App) -> Task<AppExitResponse> {
477            Task::ready(AppExitResponse::Cancel)
478        }
479    }
480
481    /// Answers once `decision` completes, as a binding whose observers ask the user.
482    struct Deciding {
483        decision: Completer<AppExitResponse>,
484    }
485
486    impl ServicesBindingOverridesObject for Deciding {
487        fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
488            let decision = app.get(self).decision.future();
489            app.spawn(async move |_cx| decision.await)
490        }
491    }
492
493    #[test]
494    fn the_binding_above_answers_in_place_of_the_default_once_it_has_decided() {
495        let cell = AppCell::new();
496        let decision = Completer::new();
497        answer_with(
498            &cell,
499            Deciding {
500                decision: decision.clone(),
501            },
502        );
503        let response = handle_request_app_exit(&cell);
504        assert_eq!(response.get(), None, "still deciding");
505
506        decision.complete(&mut cell.borrow_mut(), AppExitResponse::Cancel);
507        cell.checkpoint();
508        assert_eq!(response.get(), Some(AppExitResponse::Cancel));
509    }
510}