ferroui_android/platform/
android_activatable_lifetime.rs1use 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
9pub 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 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 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}