From a045f81e385ec5fa6f7f93454db6900e4a7374b2 Mon Sep 17 00:00:00 2001
From: daxpedda <daxpedda@gmail.com>
Date: Thu, 25 Jul 2024 13:36:34 +0200
Subject: [PATCH] Android: implement `Monitor/VideoModeHandle`

We were now able to revert `VideoModeHandle::size()` returning `Option`.
However Orbital is still missing an implementation and will return `Default`.
---
 Cargo.toml                                |   2 +
 examples/window.rs                        |  36 ++---
 src/changelog/unreleased.md               |   3 +-
 src/monitor.rs                            |  15 +-
 src/platform_impl/android/ffi.rs          | 167 +++++++++++++++++++++
 src/platform_impl/android/mod.rs          | 169 +++++++++++++++++-----
 src/platform_impl/apple/appkit/monitor.rs |   4 +-
 src/platform_impl/apple/uikit/monitor.rs  |   4 +-
 src/platform_impl/linux/mod.rs            |   2 +-
 src/platform_impl/linux/wayland/output.rs |   4 +-
 src/platform_impl/linux/x11/monitor.rs    |   4 +-
 src/platform_impl/orbital/mod.rs          |   4 +-
 src/platform_impl/web/monitor.rs          |   8 +-
 src/platform_impl/windows/monitor.rs      |   4 +-
 14 files changed, 351 insertions(+), 75 deletions(-)
 create mode 100644 src/platform_impl/android/ffi.rs

diff --git a/Cargo.toml b/Cargo.toml
index b4c5d74ec6..f2bd0ed6b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,7 +105,9 @@ softbuffer = { version = "0.4.0", default-features = false, features = [
 # Android
 [target.'cfg(target_os = "android")'.dependencies]
 android-activity = "0.6.0"
+jni = "0.21"
 ndk = { version = "0.9.0", default-features = false }
+ndk-context = "0.1"
 
 # AppKit or UIKit
 [target.'cfg(target_vendor = "apple")'.dependencies]
diff --git a/examples/window.rs b/examples/window.rs
index 78f96de610..2c791d5ac5 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -285,17 +285,14 @@ impl Application {
             }
 
             if let Some(current_mode) = monitor.current_video_mode() {
-                if let Some(PhysicalSize { width, height }) = current_mode.size() {
-                    let bits =
-                        current_mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
-                    let m_hz = current_mode
-                        .refresh_rate_millihertz()
-                        .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
-                        .unwrap_or_default();
-                    info!("  {width}x{height}{bits}{m_hz}");
-                } else {
-                    info!("  {current_mode:?}");
-                }
+                let PhysicalSize { width, height } = current_mode.size();
+                let bits =
+                    current_mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
+                let m_hz = current_mode
+                    .refresh_rate_millihertz()
+                    .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
+                    .unwrap_or_default();
+                info!("  {width}x{height}{bits}{m_hz}");
             }
 
             if let Some(PhysicalPosition { x, y }) = monitor.position() {
@@ -306,16 +303,13 @@ impl Application {
 
             info!("  Available modes (width x height x bit-depth):");
             for mode in monitor.video_modes() {
-                if let Some(PhysicalSize { width, height }) = mode.size() {
-                    let bits = mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
-                    let m_hz = mode
-                        .refresh_rate_millihertz()
-                        .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
-                        .unwrap_or_default();
-                    info!("    {width}x{height}{bits}{m_hz}");
-                } else {
-                    info!("    {mode:?}");
-                }
+                let PhysicalSize { width, height } = mode.size();
+                let bits = mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
+                let m_hz = mode
+                    .refresh_rate_millihertz()
+                    .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
+                    .unwrap_or_default();
+                info!("    {width}x{height}{bits}{m_hz}");
             }
         }
     }
diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md
index 113ff95914..da0e315716 100644
--- a/src/changelog/unreleased.md
+++ b/src/changelog/unreleased.md
@@ -55,6 +55,7 @@ changelog entry.
   information is available. This "detailed monitors" can be used in `Window::set_fullscreen()` as
   well.
 - Add `MonitorHandle::current_video_mode()`.
+- On Android, fully implement `MonitorHandle` and `VideoModeHandle`.
 
 ### Changed
 
@@ -85,7 +86,7 @@ changelog entry.
 - On Web, `CursorGrabMode::Locked` now lets `DeviceEvent::MouseMotion` return raw data, not OS
   accelerated, if the browser supports it.
 - `VideoModeHandle::refresh_rate_millihertz()` now returns an `Option<NonZeroU32>`.
-- `VideoModeHandle::size()` and `bit_depth()` now return an `Option`.
+- `VideoModeHandle::bit_depth()` now returns an `Option`.
 
 ### Removed
 
diff --git a/src/monitor.rs b/src/monitor.rs
index 2d5e1ceac6..511ed2042d 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -55,7 +55,7 @@ impl VideoModeHandle {
     ///
     /// [`Window::inner_size()`]: crate::window::Window::inner_size
     #[inline]
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
+    pub fn size(&self) -> PhysicalSize<u32> {
         self.video_mode.size()
     }
 
@@ -81,6 +81,19 @@ impl VideoModeHandle {
     }
 }
 
+impl std::fmt::Display for VideoModeHandle {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}x{} {}{}",
+            self.size().width,
+            self.size().height,
+            self.refresh_rate_millihertz().map(|rate| format!("@ {rate} mHz ")).unwrap_or_default(),
+            self.bit_depth().map(|bit_depth| format!("({bit_depth} bpp)")).unwrap_or_default(),
+        )
+    }
+}
+
 /// Handle to a monitor.
 ///
 /// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation.
diff --git a/src/platform_impl/android/ffi.rs b/src/platform_impl/android/ffi.rs
new file mode 100644
index 0000000000..998a4278a4
--- /dev/null
+++ b/src/platform_impl/android/ffi.rs
@@ -0,0 +1,167 @@
+use android_activity::AndroidApp;
+use jni::objects::{JObject, JObjectArray, JString, JValueOwned};
+use jni::{AttachGuard, JavaVM};
+
+pub fn jvm(app: &AndroidApp) -> JavaVM {
+    unsafe { JavaVM::from_raw(app.vm_as_ptr() as _) }.expect("unexpected null pointer")
+}
+
+pub fn env(jvm: &JavaVM) -> AttachGuard<'_> {
+    jvm.attach_current_thread().expect("unexpected failure accessing `JNIEnv`")
+}
+
+pub fn activity<'e>(app: &AndroidApp, _: &AttachGuard<'e>) -> Activity<'e> {
+    Activity(unsafe { JObject::from_raw(app.activity_as_ptr() as _) })
+}
+
+pub struct Activity<'e>(JObject<'e>);
+
+impl Activity<'_> {
+    pub fn window_manager<'e>(&self, env: &mut AttachGuard<'e>) -> WindowManager<'e> {
+        WindowManager(
+            env.call_method(&self.0, "getWindowManager", "()Landroid/view/WindowManager;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`Activity.getWindowManager()` is not expected to fail"),
+        )
+    }
+}
+
+pub fn context<'e>(_: &mut AttachGuard<'e>) -> Context<'e> {
+    Context(unsafe { JObject::from_raw(ndk_context::android_context().context() as _) })
+}
+
+pub struct Context<'e>(JObject<'e>);
+
+impl Context<'_> {
+    pub fn display_manager<'e>(&self, env: &mut AttachGuard<'e>) -> DisplayManager<'e> {
+        let class = env.get_object_class(&self.0).expect("failed to find `Context` class");
+        let display_service = env
+            .get_static_field(&class, "DISPLAY_SERVICE", "Ljava/lang/String;")
+            .expect("failed to find `Context.DISPLAY_SERVICE");
+        let display_manager = env
+            .call_method(&self.0, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", &[
+                (&display_service).into(),
+            ])
+            .and_then(JValueOwned::l)
+            .expect(
+                "`Context.getSystemService()` is not expected to fail with \
+                 `Context.DISPLAY_SERVICE`",
+            );
+
+        DisplayManager(display_manager)
+    }
+}
+
+pub struct DisplayManager<'e>(JObject<'e>);
+
+impl DisplayManager<'_> {
+    pub fn display<'e>(&self, env: &mut AttachGuard<'e>, display_id: i32) -> Option<Display<'e>> {
+        env.call_method(&self.0, "getDisplay", "(I)Landroid/view/Display;", &[display_id.into()])
+            .and_then(JValueOwned::l)
+            .map(Display)
+            .ok()
+    }
+
+    pub fn displays<'e>(&self, env: &mut AttachGuard<'e>) -> Vec<Display<'e>> {
+        let displays = JObjectArray::from(
+            env.call_method(&self.0, "getDisplays", "()[Landroid/view/Display;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`DisplayManager.getDisplays()` is not expected to fail"),
+        );
+
+        let length = env.get_array_length(&displays).unwrap();
+
+        (0..length)
+            .map(|index| env.get_object_array_element(&displays, index).map(Display).unwrap())
+            .collect()
+    }
+}
+
+pub struct WindowManager<'e>(JObject<'e>);
+
+impl WindowManager<'_> {
+    pub fn default_display<'e>(&self, env: &mut AttachGuard<'e>) -> Display<'e> {
+        Display(
+            env.call_method(&self.0, "getDefaultDisplay", "()Landroid/view/Display;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`WindowManager.getDefaultDisplay()` is not expected to fail"),
+        )
+    }
+}
+
+pub struct Display<'e>(JObject<'e>);
+
+impl Display<'_> {
+    pub fn default_display(env: &mut AttachGuard<'_>) -> i32 {
+        let class = env.find_class("android/view/Display").expect("failed to find `Display`");
+        env.get_static_field(&class, "DEFAULT_DISPLAY", "I;")
+            .and_then(JValueOwned::i)
+            .expect("failed to find `Display.DEFAULT_DISPLAY")
+    }
+
+    pub fn display_id(&self, env: &mut AttachGuard<'_>) -> i32 {
+        env.call_method(&self.0, "getDisplayId", "()I", &[])
+            .and_then(JValueOwned::i)
+            .expect("`Display.getDisplayId()` is not expected to fail")
+    }
+
+    pub fn name(&self, env: &mut AttachGuard<'_>) -> String {
+        let name = JString::from(
+            env.call_method(&self.0, "getName", "()Ljava/lang/String;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`Display.name()` is not expected to fail"),
+        );
+        let name = env.get_string(&name).unwrap();
+        name.to_string_lossy().to_string()
+    }
+
+    pub fn mode<'e>(&self, env: &mut AttachGuard<'e>) -> Mode<'e> {
+        Mode(
+            env.call_method(&self.0, "getMode", "()Landroid/view/Display$Mode;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`Display.getMode()` is not expected to fail"),
+        )
+    }
+
+    pub fn supported_modes<'e>(&self, env: &mut AttachGuard<'e>) -> Vec<Mode<'e>> {
+        let modes = JObjectArray::from(
+            env.call_method(&self.0, "getSupportedModes", "()[Landroid/view/Display$Mode;", &[])
+                .and_then(JValueOwned::l)
+                .expect("`Display.getSupportedModes()` is not expected to fail"),
+        );
+
+        let length = env.get_array_length(&modes).unwrap();
+
+        (0..length)
+            .map(|index| env.get_object_array_element(&modes, index).map(Mode).unwrap())
+            .collect()
+    }
+}
+
+pub struct Mode<'e>(JObject<'e>);
+
+impl Mode<'_> {
+    pub fn mode_id(&self, env: &mut AttachGuard<'_>) -> i32 {
+        env.call_method(&self.0, "getModeId", "()I", &[])
+            .and_then(JValueOwned::i)
+            .expect("`Display.Mode.getModeId()` is not expected to fail")
+    }
+
+    pub fn physical_width(&self, env: &mut AttachGuard<'_>) -> i32 {
+        env.call_method(&self.0, "getPhysicalWidth", "()I", &[])
+            .and_then(JValueOwned::i)
+            .expect("`Display.Mode.getPhysicalWidth()` is not expected to fail")
+    }
+
+    pub fn physical_height(&self, env: &mut AttachGuard<'_>) -> i32 {
+        env.call_method(&self.0, "getPhysicalHeight", "()I", &[])
+            .and_then(JValueOwned::i)
+            .expect("`Display.Mode.getPhysicalHeight()` is not expected to fail")
+    }
+
+    pub fn refresh_rate(&self, env: &mut AttachGuard<'_>) -> f32 {
+        env.call_method(&self.0, "getRefreshRate", "()F", &[])
+            .and_then(JValueOwned::f)
+            .expect("`Display.Mode.getRefreshRate()` is not expected to fail")
+    }
+}
diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs
index 7d1ccc25bc..257c489681 100644
--- a/src/platform_impl/android/mod.rs
+++ b/src/platform_impl/android/mod.rs
@@ -1,5 +1,4 @@
 use std::cell::Cell;
-use std::collections::VecDeque;
 use std::hash::Hash;
 use std::marker::PhantomData;
 use std::num::NonZeroU32;
@@ -27,6 +26,7 @@ use crate::window::{
     WindowButtons, WindowLevel,
 };
 
+mod ffi;
 mod keycodes;
 
 pub(crate) use crate::cursor::{
@@ -200,11 +200,10 @@ impl EventLoop {
                     app.window_event(self.window_target(), window_id, event);
                 },
                 MainEvent::ConfigChanged { .. } => {
-                    let monitor = MonitorHandle::new(self.android_app.clone());
-                    let old_scale_factor = monitor.scale_factor();
-                    let scale_factor = monitor.scale_factor();
+                    let old_scale_factor = scale_factor(&self.android_app);
+                    let scale_factor = scale_factor(&self.android_app);
                     if (scale_factor - old_scale_factor).abs() < f64::EPSILON {
-                        let new_inner_size = Arc::new(Mutex::new(screen_size(&self.android_app)));
+                        let new_inner_size = Arc::new(Mutex::new(surface_size(&self.android_app)));
                         let window_id = window::WindowId(WindowId);
                         let event = event::WindowEvent::ScaleFactorChanged {
                             inner_size_writer: InnerSizeWriter::new(Arc::downgrade(
@@ -587,7 +586,7 @@ impl ActiveEventLoop {
     }
 
     pub fn primary_monitor(&self) -> Option<MonitorHandle> {
-        Some(MonitorHandle::new(self.app.clone()))
+        Some(MonitorHandle::primary_monitor(self.app.clone()))
     }
 
     pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor {
@@ -595,10 +594,8 @@ impl ActiveEventLoop {
         CustomCursor { inner: PlatformCustomCursor }
     }
 
-    pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
-        let mut v = VecDeque::with_capacity(1);
-        v.push_back(MonitorHandle::new(self.app.clone()));
-        v
+    pub fn available_monitors(&self) -> Vec<MonitorHandle> {
+        MonitorHandle::available_monitors(self.app.clone())
     }
 
     #[inline]
@@ -723,21 +720,19 @@ impl Window {
     }
 
     pub fn primary_monitor(&self) -> Option<MonitorHandle> {
-        Some(MonitorHandle::new(self.app.clone()))
+        Some(MonitorHandle::primary_monitor(self.app.clone()))
     }
 
-    pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
-        let mut v = VecDeque::with_capacity(1);
-        v.push_back(MonitorHandle::new(self.app.clone()));
-        v
+    pub fn available_monitors(&self) -> Vec<MonitorHandle> {
+        MonitorHandle::available_monitors(self.app.clone())
     }
 
     pub fn current_monitor(&self) -> Option<MonitorHandle> {
-        Some(MonitorHandle::new(self.app.clone()))
+        Some(MonitorHandle::current_monitor(self.app.clone()))
     }
 
     pub fn scale_factor(&self) -> f64 {
-        MonitorHandle::new(self.app.clone()).scale_factor()
+        scale_factor(&self.app)
     }
 
     pub fn request_redraw(&self) {
@@ -767,7 +762,7 @@ impl Window {
     }
 
     pub fn outer_size(&self) -> PhysicalSize<u32> {
-        screen_size(&self.app)
+        surface_size(&self.app)
     }
 
     pub fn set_min_inner_size(&self, _: Option<Size>) {}
@@ -974,6 +969,7 @@ impl Display for OsError {
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct MonitorHandle {
     app: AndroidApp,
+    id: i32,
 }
 impl PartialOrd for MonitorHandle {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
@@ -987,12 +983,59 @@ impl Ord for MonitorHandle {
 }
 
 impl MonitorHandle {
-    pub(crate) fn new(app: AndroidApp) -> Self {
-        Self { app }
+    fn primary_monitor(app: AndroidApp) -> Self {
+        let jvm = ffi::jvm(&app);
+        let mut env = ffi::env(&jvm);
+
+        let default_display = ffi::Display::default_display(&mut env);
+        let id = ffi::context(&mut env)
+            .display_manager(&mut env)
+            .display(&mut env, default_display)
+            .unwrap()
+            .display_id(&mut env);
+
+        Self { app, id }
+    }
+
+    fn current_monitor(app: AndroidApp) -> Self {
+        let jvm = ffi::jvm(&app);
+        let mut env = ffi::env(&jvm);
+
+        // TODO: at API level 30 use `Context.getDisplay()`.
+        // <https://developer.android.com/reference/android/content/Context#getDisplay()>
+        let id = ffi::activity(&app, &env)
+            .window_manager(&mut env)
+            .default_display(&mut env)
+            .display_id(&mut env);
+
+        Self { app, id }
+    }
+
+    fn available_monitors(app: AndroidApp) -> Vec<Self> {
+        let jvm = ffi::jvm(&app);
+        let mut env = ffi::env(&jvm);
+
+        let available_monitors = ffi::context(&mut env)
+            .display_manager(&mut env)
+            .displays(&mut env)
+            .into_iter()
+            .map(|display| Self { app: app.clone(), id: display.display_id(&mut env) })
+            .collect();
+
+        available_monitors
     }
 
     pub fn name(&self) -> Option<String> {
-        None
+        let jvm = ffi::jvm(&self.app);
+        let mut env = ffi::env(&jvm);
+
+        Some(
+            ffi::context(&mut env)
+                .display_manager(&mut env)
+                .display(&mut env, self.id)
+                .unwrap()
+                .name(&mut env),
+        )
     }
 
     pub fn position(&self) -> Option<PhysicalPosition<i32>> {
@@ -1000,27 +1043,67 @@ impl MonitorHandle {
     }
 
     pub fn scale_factor(&self) -> f64 {
-        self.app.config().density().map(|dpi| dpi as f64 / 160.0).unwrap_or(1.0)
+        scale_factor(&self.app)
     }
 
     pub fn current_video_mode(&self) -> Option<VideoModeHandle> {
-        Some(VideoModeHandle { monitor: self.clone() })
+        let jvm = ffi::jvm(&self.app);
+        let mut env = ffi::env(&jvm);
+
+        let id = ffi::context(&mut env)
+            .display_manager(&mut env)
+            .display(&mut env, self.id)
+            .unwrap()
+            .mode(&mut env)
+            .mode_id(&mut env);
+
+        Some(VideoModeHandle { app: self.app.clone(), display_id: self.id, mode_id: id })
     }
 
     pub fn video_modes(&self) -> impl Iterator<Item = VideoModeHandle> {
-        // TODO: https://developer.android.com/reference/android/view/Display.Mode
-        self.current_video_mode().into_iter()
+        let jvm = ffi::jvm(&self.app);
+        let mut env = ffi::env(&jvm);
+
+        ffi::context(&mut env)
+            .display_manager(&mut env)
+            .display(&mut env, self.id)
+            .unwrap()
+            .supported_modes(&mut env)
+            .into_iter()
+            .map(|mode| VideoModeHandle {
+                app: self.app.clone(),
+                display_id: self.id,
+                mode_id: mode.mode_id(&mut env),
+            })
+            .collect::<Vec<_>>()
+            .into_iter()
     }
 }
 
 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
 pub struct VideoModeHandle {
-    monitor: MonitorHandle,
+    app: AndroidApp,
+    display_id: i32,
+    mode_id: i32,
 }
 
 impl VideoModeHandle {
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        None
+    pub fn size(&self) -> PhysicalSize<u32> {
+        let jvm = ffi::jvm(&self.app);
+        let mut env = ffi::env(&jvm);
+
+        let mode = ffi::context(&mut env)
+            .display_manager(&mut env)
+            .display(&mut env, self.display_id)
+            .unwrap()
+            .supported_modes(&mut env)
+            .into_iter()
+            .find(|mode| mode.mode_id(&mut env) == self.mode_id)
+            .unwrap();
+        let width = mode.physical_width(&mut env);
+        let height = mode.physical_height(&mut env);
+
+        PhysicalSize::new(width, height).cast()
     }
 
     pub fn bit_depth(&self) -> Option<u16> {
@@ -1028,25 +1111,41 @@ impl VideoModeHandle {
     }
 
     pub fn refresh_rate_millihertz(&self) -> Option<NonZeroU32> {
-        // TODO: get this via either `android.view.Display.getRefreshRate()` (requires JNI, but
-        // works all the way back to API level 1) or `AChoreographer_registerRefreshRateCallback`
-        // (only available since API level 30, related to the Frame Rate API).
+        // TODO: Use `AChoreographer_registerRefreshRateCallback` when on API level 30 (related to
+        // the Frame Rate API).
         //
-        // `android.view.Display.getRefreshRate()`: <https://developer.android.com/reference/android/view/Display#getRefreshRate()>
         // `AChoreographer_registerRefreshRateCallback`: <https://developer.android.com/ndk/reference/group/choreographer#achoreographer_registerrefreshratecallback>
         // Frame Rate API: <https://developer.android.com/media/optimize/performance/frame-rate>
-        None
+
+        let jvm = ffi::jvm(&self.app);
+        let mut env = ffi::env(&jvm);
+
+        let refresh_rate = ffi::context(&mut env)
+            .display_manager(&mut env)
+            .display(&mut env, self.display_id)
+            .unwrap()
+            .supported_modes(&mut env)
+            .into_iter()
+            .find(|mode| mode.mode_id(&mut env) == self.mode_id)
+            .unwrap()
+            .refresh_rate(&mut env);
+
+        NonZeroU32::new((refresh_rate * 1000.) as u32)
     }
 
     pub fn monitor(&self) -> MonitorHandle {
-        self.monitor.clone()
+        MonitorHandle { app: self.app.clone(), id: self.display_id }
     }
 }
 
-fn screen_size(app: &AndroidApp) -> PhysicalSize<u32> {
+fn surface_size(app: &AndroidApp) -> PhysicalSize<u32> {
     if let Some(native_window) = app.native_window() {
         PhysicalSize::new(native_window.width() as _, native_window.height() as _)
     } else {
         PhysicalSize::new(0, 0)
     }
 }
+
+fn scale_factor(app: &AndroidApp) -> f64 {
+    app.config().density().map(|dpi| dpi as f64 / 160.0).unwrap_or(1.0)
+}
diff --git a/src/platform_impl/apple/appkit/monitor.rs b/src/platform_impl/apple/appkit/monitor.rs
index 29091d7a27..2568954011 100644
--- a/src/platform_impl/apple/appkit/monitor.rs
+++ b/src/platform_impl/apple/appkit/monitor.rs
@@ -113,8 +113,8 @@ impl VideoModeHandle {
         }
     }
 
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        Some(self.size)
+    pub fn size(&self) -> PhysicalSize<u32> {
+        self.size
     }
 
     pub fn bit_depth(&self) -> Option<u16> {
diff --git a/src/platform_impl/apple/uikit/monitor.rs b/src/platform_impl/apple/uikit/monitor.rs
index 6b02fdc1f8..4e35854fec 100644
--- a/src/platform_impl/apple/uikit/monitor.rs
+++ b/src/platform_impl/apple/uikit/monitor.rs
@@ -66,8 +66,8 @@ impl VideoModeHandle {
         }
     }
 
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        Some(self.size.into())
+    pub fn size(&self) -> PhysicalSize<u32> {
+        self.size.into()
     }
 
     pub fn bit_depth(&self) -> Option<u16> {
diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs
index 403f82cca8..cc8dbfcb0c 100644
--- a/src/platform_impl/linux/mod.rs
+++ b/src/platform_impl/linux/mod.rs
@@ -256,7 +256,7 @@ pub enum VideoModeHandle {
 
 impl VideoModeHandle {
     #[inline]
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
+    pub fn size(&self) -> PhysicalSize<u32> {
         x11_or_wayland!(match self; VideoModeHandle(m) => m.size())
     }
 
diff --git a/src/platform_impl/linux/wayland/output.rs b/src/platform_impl/linux/wayland/output.rs
index 4e90de013e..f19a7ced1d 100644
--- a/src/platform_impl/linux/wayland/output.rs
+++ b/src/platform_impl/linux/wayland/output.rs
@@ -135,8 +135,8 @@ impl VideoModeHandle {
     }
 
     #[inline]
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        Some(self.size)
+    pub fn size(&self) -> PhysicalSize<u32> {
+        self.size
     }
 
     #[inline]
diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs
index fbbe35c481..a565c810ac 100644
--- a/src/platform_impl/linux/x11/monitor.rs
+++ b/src/platform_impl/linux/x11/monitor.rs
@@ -30,8 +30,8 @@ pub struct VideoModeHandle {
 
 impl VideoModeHandle {
     #[inline]
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        Some(self.size.into())
+    pub fn size(&self) -> PhysicalSize<u32> {
+        self.size.into()
     }
 
     #[inline]
diff --git a/src/platform_impl/orbital/mod.rs b/src/platform_impl/orbital/mod.rs
index 560358bda4..08b158959e 100644
--- a/src/platform_impl/orbital/mod.rs
+++ b/src/platform_impl/orbital/mod.rs
@@ -216,9 +216,9 @@ pub struct VideoModeHandle {
 }
 
 impl VideoModeHandle {
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
+    pub fn size(&self) -> PhysicalSize<u32> {
         // TODO
-        None
+        PhysicalSize::default()
     }
 
     pub fn bit_depth(&self) -> Option<u16> {
diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs
index faf6556ad4..7e99f2f0dc 100644
--- a/src/platform_impl/web/monitor.rs
+++ b/src/platform_impl/web/monitor.rs
@@ -226,7 +226,7 @@ impl OrientationLockError {
 pub struct VideoModeHandle(Dispatcher<Inner>);
 
 impl VideoModeHandle {
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
+    pub fn size(&self) -> PhysicalSize<u32> {
         self.0.queue(|inner| inner.size())
     }
 
@@ -330,15 +330,15 @@ impl Inner {
         matches!(self.screen, Screen::Detailed(_))
     }
 
-    fn size(&self) -> Option<PhysicalSize<u32>> {
+    fn size(&self) -> PhysicalSize<u32> {
         let width = self.screen.width().unwrap();
         let height = self.screen.height().unwrap();
 
-        Some(if let Some(Engine::Chromium) = self.engine {
+        if let Some(Engine::Chromium) = self.engine {
             PhysicalSize::new(width, height).cast()
         } else {
             LogicalSize::new(width, height).to_physical(super::web_sys::scale_factor(&self.window))
-        })
+        }
     }
 
     pub fn bit_depth(&self) -> Option<u16> {
diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs
index 125ce3706f..3f1c387986 100644
--- a/src/platform_impl/windows/monitor.rs
+++ b/src/platform_impl/windows/monitor.rs
@@ -74,8 +74,8 @@ impl VideoModeHandle {
         }
     }
 
-    pub fn size(&self) -> Option<PhysicalSize<u32>> {
-        Some(self.size.into())
+    pub fn size(&self) -> PhysicalSize<u32> {
+        self.size.into()
     }
 
     pub fn bit_depth(&self) -> Option<u16> {