Skip to main content

ferroui_android/platform/
android_screens.rs

1//! The screens of the device: the displays of the display manager.
2//!
3//! The reference asks the display objects of the system; here the Java
4//! layer describes every display in one call ([`DisplayInfo`]) and the
5//! screens are made from the descriptions, through a source the tests
6//! replace.
7
8use ferroui_base::{PixelPoint, PixelRect};
9use ferroui_controls::platform::{
10    ITopLevelImpl, PlatformHandle, PlatformScreen, Screen, ScreenOrientation, ScreensBase, ScreensBaseImpl,
11    ScreensBaseImplExt,
12};
13use std::cell::RefCell;
14use std::rc::{Rc, Weak};
15
16/// `Display.DEFAULT_DISPLAY`.
17const DEFAULT_DISPLAY: i32 = 0;
18
19/// `Surface.ROTATION_*`.
20pub(crate) mod surface_orientation {
21    pub const ROTATION_0: i32 = 0;
22    pub const ROTATION_90: i32 = 1;
23    pub const ROTATION_180: i32 = 2;
24    pub const ROTATION_270: i32 = 3;
25}
26
27/// `Configuration.ORIENTATION_*`.
28pub(crate) mod android_orientation {
29    pub const PORTRAIT: i32 = 1;
30    pub const LANDSCAPE: i32 = 2;
31    pub const SQUARE: i32 = 3;
32}
33
34/// What the Java layer tells about a display.
35#[derive(Clone, Debug, PartialEq)]
36pub(crate) struct DisplayInfo {
37    pub display_id: i32,
38    pub name: String,
39    /// `Display.getRotation`.
40    pub rotation: i32,
41    /// Whether the bounds are the maximum window metrics of the display
42    /// (API 30 and later); otherwise they are its real metrics.
43    pub has_window_metrics: bool,
44    pub bounds: PixelRect,
45    /// The density of the configuration over the default density, or the
46    /// density of the real metrics.
47    pub scaling: f64,
48    /// The orientation of the configuration of the display; 0 when it is
49    /// not known.
50    pub orientation: i32,
51}
52
53/// The number of values the Java layer gives for each display.
54const DISPLAY_LENGTH: usize = 9;
55
56impl DisplayInfo {
57    /// Reads the descriptions of `PlatformHelper.getDisplays` and the names
58    /// of `PlatformHelper.getDisplayNames`.
59    pub fn from_arrays(values: &[f32], names: &[String]) -> Vec<DisplayInfo> {
60        values
61            .chunks_exact(DISPLAY_LENGTH)
62            .enumerate()
63            .map(|(index, display)| DisplayInfo {
64                display_id: display[0] as i32,
65                name: names.get(index).cloned().unwrap_or_default(),
66                rotation: display[1] as i32,
67                has_window_metrics: display[2] != 0.0,
68                bounds: PixelRect::new(display[3] as i32, display[4] as i32, display[5] as i32, display[6] as i32),
69                scaling: f64::from(display[7]),
70                orientation: display[8] as i32,
71            })
72            .collect()
73    }
74
75    /// The orientation of the display at rotation zero.
76    pub(crate) fn natural_orientation(&self) -> ScreenOrientation {
77        if !self.has_window_metrics {
78            return ScreenOrientation::Portrait;
79        }
80
81        if self.orientation == android_orientation::SQUARE {
82            ScreenOrientation::None
83        } else if self.rotation == surface_orientation::ROTATION_0 || self.rotation == surface_orientation::ROTATION_180
84        {
85            if self.orientation == android_orientation::LANDSCAPE {
86                ScreenOrientation::Landscape
87            } else {
88                ScreenOrientation::Portrait
89            }
90        } else if self.orientation == android_orientation::PORTRAIT {
91            ScreenOrientation::Landscape
92        } else {
93            ScreenOrientation::Portrait
94        }
95    }
96
97    pub(crate) fn current_orientation(&self) -> ScreenOrientation {
98        use surface_orientation::{ROTATION_0, ROTATION_180, ROTATION_270, ROTATION_90};
99
100        match (self.rotation, self.natural_orientation()) {
101            (_, ScreenOrientation::None) => ScreenOrientation::None,
102            (ROTATION_0, ScreenOrientation::Landscape) => ScreenOrientation::Landscape,
103            (ROTATION_90, ScreenOrientation::Landscape) => ScreenOrientation::Portrait,
104            (ROTATION_180, ScreenOrientation::Landscape) => ScreenOrientation::LandscapeFlipped,
105            (ROTATION_270, ScreenOrientation::Landscape) => ScreenOrientation::PortraitFlipped,
106            (ROTATION_0, _) => ScreenOrientation::Portrait,
107            (ROTATION_90, _) => ScreenOrientation::Landscape,
108            (ROTATION_180, _) => ScreenOrientation::PortraitFlipped,
109            (ROTATION_270, _) => ScreenOrientation::LandscapeFlipped,
110            _ => ScreenOrientation::Portrait,
111        }
112    }
113
114    /// The working area: the bounds without the system bars of an empty set
115    /// of insets, which is what the reference computes (it builds the
116    /// insets it asks, so every inset is zero).
117    pub(crate) fn working_area(&self) -> PixelRect {
118        let (left, top, right, bottom) = (0, 0, 0, 0);
119        PixelRect::new(
120            self.bounds.x + left,
121            self.bounds.y + top,
122            self.bounds.width - (left + right),
123            self.bounds.height - (top + bottom),
124        )
125    }
126}
127
128/// Where the screens come from.
129pub(crate) trait IDisplaySource {
130    /// The displays of the device now.
131    fn displays(&self) -> Vec<DisplayInfo>;
132
133    /// The id of the display a top-level is on; `None` when it is on none.
134    fn display_of_top_level(&self, top_level: &dyn ITopLevelImpl) -> Option<i32>;
135}
136
137/// A screen of the device.
138pub struct AndroidScreen {
139    base: PlatformScreen,
140    display_id: i32,
141}
142
143impl AndroidScreen {
144    fn new(display_id: i32) -> Self {
145        Self {
146            base: PlatformScreen::new(Rc::new(PlatformHandle::new(display_id as isize, Some("DisplayId")))),
147            display_id,
148        }
149    }
150
151    /// The id of the display of the screen.
152    pub fn display_id(&self) -> i32 {
153        self.display_id
154    }
155
156    pub(crate) fn refresh(&self, display: &DisplayInfo) {
157        self.base.set_display_name(Some(display.name.clone()));
158        self.base.set_is_primary(display.display_id == DEFAULT_DISPLAY);
159
160        if display.has_window_metrics {
161            self.base.set_bounds(display.bounds);
162            self.base.set_working_area(display.working_area());
163            // The reference leaves the scaling alone when the context has no configuration.
164            if display.scaling > 0.0 {
165                self.base.set_scaling(display.scaling);
166            }
167        } else {
168            self.base.set_scaling(display.scaling);
169            self.base.set_bounds(display.bounds);
170            self.base.set_working_area(display.bounds);
171        }
172
173        self.base.set_current_orientation(display.current_orientation());
174    }
175}
176
177impl AsRef<PlatformScreen> for AndroidScreen {
178    fn as_ref(&self) -> &PlatformScreen {
179        &self.base
180    }
181}
182
183/// The screens of the device, keyed by the id of their display.
184pub struct AndroidScreens {
185    base: ScreensBase<i32, AndroidScreen>,
186    weak_self: Weak<AndroidScreens>,
187    source: Box<dyn IDisplaySource>,
188    /// The displays as the last enumeration found them, for the refresh of
189    /// the screens that follows it.
190    displays: RefCell<Vec<DisplayInfo>>,
191}
192
193impl AndroidScreens {
194    pub(crate) fn with_source(source: Box<dyn IDisplaySource>) -> Rc<AndroidScreens> {
195        Rc::new_cyclic(|weak_self| AndroidScreens {
196            base: ScreensBase::new(),
197            weak_self: weak_self.clone(),
198            source,
199            displays: RefCell::new(Vec::new()),
200        })
201    }
202
203    /// The displays changed, or the configuration did.
204    pub(crate) fn on_changed(&self) {
205        if let Some(this) = self.weak_self.upgrade() {
206            ScreensBaseImplExt::on_changed(&this);
207        }
208    }
209}
210
211impl ScreensBaseImpl for AndroidScreens {
212    type Key = i32;
213    type Screen = AndroidScreen;
214
215    fn screens_base(&self) -> &ScreensBase<i32, AndroidScreen> {
216        &self.base
217    }
218
219    fn get_all_screen_keys(&self) -> Vec<i32> {
220        let displays = self.source.displays();
221        let keys = displays.iter().map(|display| display.display_id).collect();
222        *self.displays.borrow_mut() = displays;
223        keys
224    }
225
226    fn create_screen_from_key(&self, key: &i32) -> Rc<AndroidScreen> {
227        Rc::new(AndroidScreen::new(*key))
228    }
229
230    fn screen_changed(&self, screen: &Rc<AndroidScreen>) {
231        let display = self.displays.borrow().iter().find(|display| display.display_id == screen.display_id).cloned();
232        if let Some(display) = display {
233            screen.refresh(&display);
234        }
235    }
236
237    fn screen_from_top_level_core(&self, top_level: &dyn ITopLevelImpl) -> Option<Rc<Screen>> {
238        let display = self.source.display_of_top_level(top_level)?;
239        self.try_get_screen(&display).map(|screen| screen.base.screen().clone())
240    }
241
242    fn screen_from_point_core(&self, _point: PixelPoint) -> Option<Rc<Screen>> {
243        None
244    }
245
246    fn screen_from_rect_core(&self, _rect: PixelRect) -> Option<Rc<Screen>> {
247        None
248    }
249}
250
251#[cfg(target_os = "android")]
252mod imp {
253    use super::{AndroidScreens, DisplayInfo, IDisplaySource};
254    use crate::interop::java::{
255        call_static_object, call_static_void, float_array_of, string_array_of, JavaClass, JavaObject, JavaValue,
256    };
257    use crate::interop::natives::{next_handle, PLATFORM_HELPER};
258    use crate::platform::skia_platform::TopLevelImpl;
259    use ferroui_controls::platform::ITopLevelImpl;
260    use std::cell::RefCell;
261    use std::collections::HashMap;
262    use std::rc::{Rc, Weak};
263
264    ferroui_base::ferro_thread_local! {
265        static SCREENS: RefCell<HashMap<i64, Weak<AndroidScreens>>> = RefCell::new(HashMap::new());
266    }
267
268    /// The displays of the display manager of a context, with the listener
269    /// that reports their changes.
270    struct ContextDisplaySource {
271        context: JavaObject,
272        handle: i64,
273        listener: Option<JavaObject>,
274    }
275
276    impl IDisplaySource for ContextDisplaySource {
277        fn displays(&self) -> Vec<DisplayInfo> {
278            let helper = JavaClass::find(PLATFORM_HELPER);
279            let context = [JavaValue::Object(Some(&self.context))];
280            let values = call_static_object(&helper, "getDisplays", "(Landroid/content/Context;)[F", &context);
281            let names = call_static_object(
282                &helper,
283                "getDisplayNames",
284                "(Landroid/content/Context;)[Ljava/lang/String;",
285                &context,
286            );
287            match (values, names) {
288                (Some(values), Some(names)) => {
289                    DisplayInfo::from_arrays(&float_array_of(&values), &string_array_of(&names))
290                }
291                _ => Vec::new(),
292            }
293        }
294
295        fn display_of_top_level(&self, top_level: &dyn ITopLevelImpl) -> Option<i32> {
296            top_level.as_any().downcast_ref::<TopLevelImpl>()?.display_id()
297        }
298    }
299
300    impl Drop for ContextDisplaySource {
301        fn drop(&mut self) {
302            SCREENS.with(|screens| screens.borrow_mut().remove(&self.handle));
303            if let Some(listener) = &self.listener {
304                call_static_void(
305                    &JavaClass::find(PLATFORM_HELPER),
306                    "unregisterDisplayListener",
307                    "(Landroid/content/Context;Ljava/lang/Object;)V",
308                    &[JavaValue::Object(Some(&self.context)), JavaValue::Object(Some(listener))],
309                );
310            }
311        }
312    }
313
314    impl AndroidScreens {
315        /// The screens of the display manager of `context`, which follow
316        /// its display listener.
317        pub(crate) fn new(context: &JavaObject) -> Rc<AndroidScreens> {
318            let handle = next_handle();
319            let listener = call_static_object(
320                &JavaClass::find(PLATFORM_HELPER),
321                "registerDisplayListener",
322                "(Landroid/content/Context;J)Ljava/lang/Object;",
323                &[JavaValue::Object(Some(context)), JavaValue::Long(handle)],
324            )
325            .map(|listener| listener.to_global());
326            let screens = AndroidScreens::with_source(Box::new(ContextDisplaySource {
327                context: context.clone(),
328                handle,
329                listener,
330            }));
331            SCREENS.with(|all| all.borrow_mut().insert(handle, Rc::downgrade(&screens)));
332            screens
333        }
334
335        /// The display listener registered with `handle` reported a change.
336        pub(crate) fn on_displays_changed(handle: i64) {
337            let screens = SCREENS.with(|all| all.borrow().get(&handle).and_then(Weak::upgrade));
338            if let Some(screens) = screens {
339                screens.on_changed();
340            }
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    // Not from the reference, which has no tests of the screens.
348    use super::*;
349    use ferroui_controls::platform::IScreenImpl;
350    use std::cell::Cell;
351
352    fn display(display_id: i32, rotation: i32, orientation: i32) -> DisplayInfo {
353        DisplayInfo {
354            display_id,
355            name: format!("Display {display_id}"),
356            rotation,
357            has_window_metrics: true,
358            bounds: PixelRect::new(0, 0, 1080, 2400),
359            scaling: 2.625,
360            orientation,
361        }
362    }
363
364    struct Source {
365        displays: Rc<RefCell<Vec<DisplayInfo>>>,
366        top_level_display: Rc<Cell<Option<i32>>>,
367    }
368
369    impl IDisplaySource for Source {
370        fn displays(&self) -> Vec<DisplayInfo> {
371            self.displays.borrow().clone()
372        }
373
374        fn display_of_top_level(&self, _top_level: &dyn ITopLevelImpl) -> Option<i32> {
375            self.top_level_display.get()
376        }
377    }
378
379    #[test]
380    fn the_arrays_of_the_java_layer_are_read_per_display() {
381        let values = [
382            0.0, 1.0, 1.0, 0.0, 0.0, 2400.0, 1080.0, 2.625, 2.0, //
383            7.0, 0.0, 0.0, 0.0, 0.0, 1920.0, 1080.0, 1.0, 0.0,
384        ];
385        let displays = DisplayInfo::from_arrays(&values, &["Built-in".to_string()]);
386
387        assert_eq!(displays.len(), 2);
388        assert_eq!(displays[0].display_id, 0);
389        assert_eq!(displays[0].name, "Built-in");
390        assert_eq!(displays[0].rotation, surface_orientation::ROTATION_90);
391        assert!(displays[0].has_window_metrics);
392        assert_eq!(displays[0].bounds, PixelRect::new(0, 0, 2400, 1080));
393        assert_eq!(displays[0].scaling, 2.625);
394        assert_eq!(displays[0].orientation, android_orientation::LANDSCAPE);
395        assert_eq!(displays[1].display_id, 7);
396        assert_eq!(displays[1].name, "");
397        assert!(!displays[1].has_window_metrics);
398    }
399
400    #[test]
401    fn a_phone_is_portrait_at_rotation_zero_and_landscape_when_turned() {
402        use android_orientation::{LANDSCAPE, PORTRAIT};
403        use surface_orientation::{ROTATION_0, ROTATION_180, ROTATION_270, ROTATION_90};
404
405        // The configuration follows the rotation: a phone held upright is portrait.
406        assert_eq!(display(0, ROTATION_0, PORTRAIT).current_orientation(), ScreenOrientation::Portrait);
407        assert_eq!(display(0, ROTATION_90, LANDSCAPE).current_orientation(), ScreenOrientation::Landscape);
408        assert_eq!(display(0, ROTATION_180, PORTRAIT).current_orientation(), ScreenOrientation::PortraitFlipped);
409        assert_eq!(display(0, ROTATION_270, LANDSCAPE).current_orientation(), ScreenOrientation::LandscapeFlipped);
410    }
411
412    #[test]
413    fn a_tablet_is_landscape_at_rotation_zero() {
414        use android_orientation::{LANDSCAPE, PORTRAIT};
415        use surface_orientation::{ROTATION_0, ROTATION_180, ROTATION_270, ROTATION_90};
416
417        assert_eq!(display(0, ROTATION_0, LANDSCAPE).current_orientation(), ScreenOrientation::Landscape);
418        assert_eq!(display(0, ROTATION_90, PORTRAIT).current_orientation(), ScreenOrientation::Portrait);
419        assert_eq!(display(0, ROTATION_180, LANDSCAPE).current_orientation(), ScreenOrientation::LandscapeFlipped);
420        assert_eq!(display(0, ROTATION_270, PORTRAIT).current_orientation(), ScreenOrientation::PortraitFlipped);
421    }
422
423    #[test]
424    fn a_square_display_has_no_orientation_and_real_metrics_are_portrait() {
425        assert_eq!(display(0, 1, android_orientation::SQUARE).current_orientation(), ScreenOrientation::None);
426
427        let mut old = display(0, surface_orientation::ROTATION_90, 0);
428        old.has_window_metrics = false;
429        assert_eq!(old.natural_orientation(), ScreenOrientation::Portrait);
430        assert_eq!(old.current_orientation(), ScreenOrientation::Landscape);
431    }
432
433    #[test]
434    fn the_screens_follow_the_displays_of_the_source() {
435        let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
436        let displays = Rc::new(RefCell::new(vec![display(0, 0, android_orientation::PORTRAIT)]));
437        let top_level_display = Rc::new(Cell::new(None));
438        let screens = AndroidScreens::with_source(Box::new(Source {
439            displays: displays.clone(),
440            top_level_display: top_level_display.clone(),
441        }));
442
443        let all = screens.all_screens();
444        assert_eq!(all.len(), 1);
445        assert!(all[0].is_primary());
446        assert_eq!(all[0].display_name().as_deref(), Some("Display 0"));
447        assert_eq!(all[0].bounds(), PixelRect::new(0, 0, 1080, 2400));
448        assert_eq!(all[0].working_area(), PixelRect::new(0, 0, 1080, 2400));
449        assert_eq!(all[0].scaling(), 2.625);
450        assert_eq!(all[0].current_orientation(), ScreenOrientation::Portrait);
451
452        // A second display appears and the first is turned.
453        let changed = Rc::new(Cell::new(0));
454        screens.set_changed(Some(Rc::new({
455            let changed = changed.clone();
456            move || changed.set(changed.get() + 1)
457        })));
458        {
459            let mut displays = displays.borrow_mut();
460            displays[0].rotation = surface_orientation::ROTATION_90;
461            displays[0].orientation = android_orientation::LANDSCAPE;
462            displays[0].bounds = PixelRect::new(0, 0, 2400, 1080);
463            displays.push(display(3, 0, android_orientation::LANDSCAPE));
464        }
465        screens.on_changed();
466        screens.on_changed();
467        // The changes are gathered into one job of the dispatcher.
468        assert_eq!(changed.get(), 0);
469        ferroui_base::threading::Dispatcher::ui_thread().run_jobs(None);
470
471        assert_eq!(changed.get(), 1);
472        let all = screens.all_screens();
473        assert_eq!(all.len(), 2);
474        assert_eq!(all[0].current_orientation(), ScreenOrientation::Landscape);
475        assert_eq!(all[0].bounds(), PixelRect::new(0, 0, 2400, 1080));
476        assert!(!all[1].is_primary());
477
478        assert!(screens.screen_from_point(PixelPoint::new(10, 10)).is_none());
479        assert!(screens.screen_from_rect(PixelRect::new(0, 0, 10, 10)).is_none());
480    }
481}