Skip to main content

ferroui_android/platform/
android_activatable_lifetime.rs

1use crate::i_ferro_activity::IFerroActivity;
2use ferroui_base::reactive::IDisposable;
3use ferroui_controls::application_lifetimes::{
4    ActivatableLifetimeBase, ActivatedEventArgs, ActivationKind, IActivatableLifetime,
5};
6use std::cell::RefCell;
7use std::rc::Rc;
8
9/// Activation of the application follows its activities.
10pub struct AndroidActivatableLifetime {
11    base: Rc<ActivatableLifetimeBase>,
12    main_activity: RefCell<Option<Rc<dyn IFerroActivity>>>,
13    intend_activity: RefCell<Option<Rc<dyn IFerroActivity>>>,
14    main_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
15    intend_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
16}
17
18fn dispose_all(subscriptions: &RefCell<Vec<Rc<dyn IDisposable>>>) {
19    let subscriptions = std::mem::take(&mut *subscriptions.borrow_mut());
20    for subscription in subscriptions {
21        subscription.dispose();
22    }
23}
24
25fn is_intend_activation(kind: ActivationKind) -> bool {
26    matches!(kind, ActivationKind::File | ActivationKind::OpenUri)
27}
28
29impl AndroidActivatableLifetime {
30    pub fn new() -> Rc<Self> {
31        Rc::new(Self {
32            base: ActivatableLifetimeBase::new(),
33            main_activity: RefCell::new(None),
34            intend_activity: RefCell::new(None),
35            main_subscriptions: RefCell::new(Vec::new()),
36            intend_subscriptions: RefCell::new(Vec::new()),
37        })
38    }
39
40    /// While we primarily handle main activity lifecycle events.
41    /// Any secondary activity might send protocol or file activation.
42    pub fn current_intend_activity(&self) -> Option<Rc<dyn IFerroActivity>> {
43        self.intend_activity.borrow().clone()
44    }
45
46    pub fn set_current_intend_activity(&self, value: Option<Rc<dyn IFerroActivity>>) {
47        dispose_all(&self.intend_subscriptions);
48
49        let previous = self.intend_activity.replace(value.clone());
50        drop(previous);
51
52        if let Some(intend_activity) = value {
53            let base = self.base.clone();
54            let subscription = intend_activity.activated(Rc::new(move |e: &ActivatedEventArgs| {
55                if is_intend_activation(e.kind()) {
56                    base.on_activated(e.clone());
57                }
58            }));
59            self.intend_subscriptions.borrow_mut().push(subscription);
60        }
61    }
62
63    pub fn current_main_activity(&self) -> Option<Rc<dyn IFerroActivity>> {
64        self.main_activity.borrow().clone()
65    }
66
67    pub fn set_current_main_activity(&self, value: Option<Rc<dyn IFerroActivity>>) {
68        dispose_all(&self.main_subscriptions);
69
70        let previous = self.main_activity.replace(value.clone());
71        drop(previous);
72
73        if let Some(main_activity) = value {
74            let base = self.base.clone();
75            let activated = main_activity.activated(Rc::new(move |e: &ActivatedEventArgs| {
76                if !is_intend_activation(e.kind()) {
77                    base.on_activated(e.clone());
78                }
79            }));
80            let base = self.base.clone();
81            let deactivated =
82                main_activity.deactivated(Rc::new(move |e: &ActivatedEventArgs| base.on_deactivated(e.clone())));
83            self.main_subscriptions.borrow_mut().extend([activated, deactivated]);
84        }
85    }
86}
87
88impl IActivatableLifetime for AndroidActivatableLifetime {
89    fn activated(&self, handler: Rc<dyn Fn(&ActivatedEventArgs)>) -> Rc<dyn IDisposable> {
90        self.base.activated(handler)
91    }
92
93    fn deactivated(&self, handler: Rc<dyn Fn(&ActivatedEventArgs)>) -> Rc<dyn IDisposable> {
94        self.base.deactivated(handler)
95    }
96
97    fn try_leave_background(&self) -> bool {
98        self.base.try_leave_background()
99    }
100
101    fn try_enter_background(&self) -> bool {
102        let main_activity = self.main_activity.borrow().clone();
103        main_activity.is_some_and(|main_activity| main_activity.move_task_to_back(true))
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    // Not from the reference, which has no tests of the lifetime.
110    use super::*;
111    use ferroui_base::reactive::Disposable;
112    use ferroui_base::utilities::HandlerList;
113    use ferroui_base::BoxedValue;
114    use std::cell::Cell;
115
116    #[derive(Default)]
117    struct Activity {
118        activated: Rc<HandlerList<dyn Fn(&ActivatedEventArgs)>>,
119        deactivated: Rc<HandlerList<dyn Fn(&ActivatedEventArgs)>>,
120        moved_to_back: Cell<bool>,
121    }
122
123    impl Activity {
124        fn raise(handlers: &HandlerList<dyn Fn(&ActivatedEventArgs)>, kind: ActivationKind) {
125            for (_, handler) in handlers.snapshot().iter() {
126                handler(&ActivatedEventArgs::new(kind));
127            }
128        }
129    }
130
131    impl crate::i_activity_result_handler::IActivityResultHandler for Activity {
132        fn activity_result(&self) -> Option<crate::i_activity_result_handler::ActivityResultHandler> {
133            None
134        }
135
136        fn set_activity_result(&self, _value: Option<crate::i_activity_result_handler::ActivityResultHandler>) {}
137
138        fn request_permissions_result(
139            &self,
140        ) -> Option<crate::i_activity_result_handler::RequestPermissionsResultHandler> {
141            None
142        }
143
144        fn set_request_permissions_result(
145            &self,
146            _value: Option<crate::i_activity_result_handler::RequestPermissionsResultHandler>,
147        ) {
148        }
149    }
150
151    impl crate::i_android_navigation_service::IActivityNavigationService for Activity {
152        fn back_requested(
153            &self,
154            _handler: Rc<dyn Fn(&crate::i_android_navigation_service::AndroidBackRequestedEventArgs)>,
155        ) -> Rc<dyn IDisposable> {
156            ferroui_base::reactive::Disposable::create(|| {})
157        }
158    }
159
160    impl IFerroActivity for Activity {
161        fn content(&self) -> Option<BoxedValue> {
162            None
163        }
164
165        fn set_content(&self, _value: Option<BoxedValue>) {}
166
167        fn activated(&self, handler: Rc<dyn Fn(&ActivatedEventArgs)>) -> Rc<dyn IDisposable> {
168            let token = self.activated.add(handler);
169            let handlers = self.activated.clone();
170            Disposable::create(move || {
171                handlers.remove(token);
172            })
173        }
174
175        fn deactivated(&self, handler: Rc<dyn Fn(&ActivatedEventArgs)>) -> Rc<dyn IDisposable> {
176            let token = self.deactivated.add(handler);
177            let handlers = self.deactivated.clone();
178            Disposable::create(move || {
179                handlers.remove(token);
180            })
181        }
182
183        fn move_task_to_back(&self, non_root: bool) -> bool {
184            self.moved_to_back.set(non_root);
185            true
186        }
187    }
188
189    fn counted(lifetime: &AndroidActivatableLifetime) -> (Rc<RefCell<Vec<ActivationKind>>>, Rc<Cell<i32>>) {
190        let activated = Rc::new(RefCell::new(Vec::new()));
191        let deactivated = Rc::new(Cell::new(0));
192        let _ = lifetime.activated(Rc::new({
193            let activated = activated.clone();
194            move |e: &ActivatedEventArgs| activated.borrow_mut().push(e.kind())
195        }));
196        let _ = lifetime.deactivated(Rc::new({
197            let deactivated = deactivated.clone();
198            move |_: &ActivatedEventArgs| deactivated.set(deactivated.get() + 1)
199        }));
200        (activated, deactivated)
201    }
202
203    #[test]
204    fn the_main_activity_activates_and_deactivates_but_not_for_an_intent() {
205        let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
206        let lifetime = AndroidActivatableLifetime::new();
207        let (activated, deactivated) = counted(&lifetime);
208        let activity = Rc::new(Activity::default());
209        lifetime.set_current_main_activity(Some(activity.clone()));
210
211        Activity::raise(&activity.activated, ActivationKind::Background);
212        Activity::raise(&activity.activated, ActivationKind::OpenUri);
213        Activity::raise(&activity.deactivated, ActivationKind::Background);
214
215        assert_eq!(*activated.borrow(), [ActivationKind::Background]);
216        assert_eq!(deactivated.get(), 1);
217    }
218
219    #[test]
220    fn the_intent_activity_activates_only_for_an_intent() {
221        let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
222        let lifetime = AndroidActivatableLifetime::new();
223        let (activated, _) = counted(&lifetime);
224        let activity = Rc::new(Activity::default());
225        lifetime.set_current_intend_activity(Some(activity.clone()));
226
227        Activity::raise(&activity.activated, ActivationKind::Background);
228        Activity::raise(&activity.activated, ActivationKind::File);
229
230        assert_eq!(*activated.borrow(), [ActivationKind::File]);
231    }
232
233    #[test]
234    fn an_activity_that_is_replaced_is_no_longer_listened_to() {
235        let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
236        let lifetime = AndroidActivatableLifetime::new();
237        let (activated, deactivated) = counted(&lifetime);
238        let activity = Rc::new(Activity::default());
239        lifetime.set_current_main_activity(Some(activity.clone()));
240        lifetime.set_current_main_activity(None);
241
242        Activity::raise(&activity.activated, ActivationKind::Background);
243        Activity::raise(&activity.deactivated, ActivationKind::Background);
244
245        assert!(activated.borrow().is_empty());
246        assert_eq!(deactivated.get(), 0);
247        assert!(activity.activated.is_empty());
248    }
249
250    #[test]
251    fn entering_the_background_moves_the_task_of_the_main_activity_back() {
252        let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
253        let lifetime = AndroidActivatableLifetime::new();
254        assert!(!lifetime.try_enter_background());
255
256        let activity = Rc::new(Activity::default());
257        lifetime.set_current_main_activity(Some(activity.clone()));
258
259        assert!(lifetime.try_enter_background());
260        assert!(activity.moved_to_back.get());
261    }
262}