From 1f2648f48c19faef88f8c68655d2ebf9b7fad898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 7 Jan 2024 17:40:10 +0100 Subject: [PATCH] qr-code-scanner: Make Camera and CameraPaintable subclassable per-OS --- Cargo.toml | 23 +-- meson.build | 8 +- src/contrib/mod.rs | 2 +- src/contrib/qr_code_scanner/camera.rs | 123 -------------- .../camera_paintable/linux.rs} | 40 ++--- .../camera/camera_paintable/mod.rs | 101 ++++++++++++ src/contrib/qr_code_scanner/camera/linux.rs | 119 ++++++++++++++ src/contrib/qr_code_scanner/camera/mod.rs | 155 ++++++++++++++++++ src/contrib/qr_code_scanner/mod.rs | 3 +- .../qr_code_scanner/qr_code_detector.rs | 2 +- src/prelude.rs | 1 + src/session/model/verification/mod.rs | 2 +- 12 files changed, 413 insertions(+), 166 deletions(-) delete mode 100644 src/contrib/qr_code_scanner/camera.rs rename src/contrib/qr_code_scanner/{camera_paintable.rs => camera/camera_paintable/linux.rs} (89%) create mode 100644 src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs create mode 100644 src/contrib/qr_code_scanner/camera/linux.rs create mode 100644 src/contrib/qr_code_scanner/camera/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 4f2bd262..298e5ace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,6 @@ codegen-units = 16 # Please keep dependencies sorted. [dependencies] -ashpd = { version = "0.6", default-features = false, features = [ - "pipewire", - "tracing", - "tokio", -] } djb_hash = "0.1" eyeball-im = "0.4" futures-channel = "0.3" @@ -40,11 +35,6 @@ indexmap = "2" mime = "0.3" mime_guess = "2" once_cell = "1" -oo7 = { version = "0.2", default-features = false, features = [ - "native_crypto", - "tokio", - "tracing", -] } pulldown-cmark = "0.9" qrcode = "0.12" rand = "0.8" @@ -106,3 +96,16 @@ features = [ "compat-get-3pids", "html", ] + +# Linux-only dependencies. +[target.'cfg(target_os = "linux")'.dependencies] +ashpd = { version = "0.6", default-features = false, features = [ + "pipewire", + "tracing", + "tokio", +] } +oo7 = { version = "0.2", default-features = false, features = [ + "native_crypto", + "tokio", + "tracing", +] } diff --git a/meson.build b/meson.build index 92633864..a81e3d5d 100644 --- a/meson.build +++ b/meson.build @@ -39,11 +39,15 @@ dependency( fallback: ['gtksourceview', 'gtksource_dep'], default_options: ['gtk_doc=false', 'sysprof=false', 'gir=false', 'vapi=false', 'install_tests=false'] ) -dependency('libpipewire-0.3', version: '>= 0.3.0') dependency('openssl', version: '>= 1.0.1') dependency('shumate-1.0', version: '>= 1.0.0') dependency('sqlite3', version: '>= 3.24.0') -dependency('xdg-desktop-portal', version: '>= 1.14.1') + +# Linux-only dependencies +if build_machine.system() == 'linux' + dependency('libpipewire-0.3', version: '>= 0.3.0') + dependency('xdg-desktop-portal', version: '>= 1.14.1') +endif glib_compile_resources = find_program('glib-compile-resources', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true) diff --git a/src/contrib/mod.rs b/src/contrib/mod.rs index 723be85a..8946a1d3 100644 --- a/src/contrib/mod.rs +++ b/src/contrib/mod.rs @@ -3,5 +3,5 @@ mod qr_code_scanner; pub use self::{ qr_code::QRCode, - qr_code_scanner::{Camera, QrCodeScanner}, + qr_code_scanner::{Camera, CameraExt, QrCodeScanner}, }; diff --git a/src/contrib/qr_code_scanner/camera.rs b/src/contrib/qr_code_scanner/camera.rs deleted file mode 100644 index 47401a27..00000000 --- a/src/contrib/qr_code_scanner/camera.rs +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -use std::time::Duration; - -use ashpd::desktop::camera; -use gtk::{glib, subclass::prelude::*}; -use once_cell::sync::Lazy; -use tracing::error; - -use super::camera_paintable::CameraPaintable; -use crate::{spawn_tokio, utils::timeout_future}; - -mod imp { - use super::*; - - #[derive(Debug, Default)] - pub struct Camera { - pub paintable: glib::WeakRef, - } - - #[glib::object_subclass] - impl ObjectSubclass for Camera { - const NAME: &'static str = "Camera"; - type Type = super::Camera; - } - - impl ObjectImpl for Camera {} -} - -glib::wrapper! { - pub struct Camera(ObjectSubclass); -} - -impl Camera { - /// Create a new `Camera`. - /// - /// Use `Camera::default()` to get a shared GObject. - fn new() -> Self { - glib::Object::new() - } - - /// Ask the system whether cameras are available. - pub async fn has_cameras(&self) -> bool { - let handle = spawn_tokio!(async move { - let camera = match camera::Camera::new().await { - Ok(camera) => camera, - Err(error) => { - error!("Failed to create instance of camera proxy: {error}"); - return false; - } - }; - - match camera.is_present().await { - Ok(is_present) => is_present, - Err(error) => { - error!("Failed to check whether system has cameras: {error}"); - false - } - } - }); - let abort_handle = handle.abort_handle(); - - match timeout_future(Duration::from_secs(1), handle).await { - Ok(is_present) => is_present.expect("The task should not have been aborted"), - Err(_) => { - abort_handle.abort(); - error!("Failed to check whether system has cameras: the request timed out"); - false - } - } - } - - /// Get the a `gdk::Paintable` displaying the content of a camera. - /// - /// Panics if not called from the `MainContext` where GTK is running. - pub async fn paintable(&self) -> Option { - // We need to make sure that the Paintable is taken only from the MainContext - assert!(glib::MainContext::default().is_owner()); - let imp = self.imp(); - - if let Some(paintable) = imp.paintable.upgrade() { - return Some(paintable); - } - - let handle = spawn_tokio!(async move { camera::request().await }); - let abort_handle = handle.abort_handle(); - - match timeout_future(Duration::from_secs(1), handle).await { - Ok(tokio_res) => match tokio_res.expect("The task should not have been aborted") { - Ok(Some((fd, streams))) => { - let paintable = CameraPaintable::new(fd, streams).await; - imp.paintable.set(Some(&paintable)); - - Some(paintable) - } - Ok(None) => { - error!("Failed to request access to cameras: the response is empty"); - None - } - Err(error) => { - error!("Failed to request access to cameras: {error}"); - None - } - }, - Err(_) => { - // Error because we reached the timeout. - abort_handle.abort(); - error!("Failed to request access to cameras: the request timed out"); - None - } - } - } -} - -impl Default for Camera { - fn default() -> Self { - static CAMERA: Lazy = Lazy::new(Camera::new); - - CAMERA.to_owned() - } -} - -unsafe impl Send for Camera {} -unsafe impl Sync for Camera {} diff --git a/src/contrib/qr_code_scanner/camera_paintable.rs b/src/contrib/qr_code_scanner/camera/camera_paintable/linux.rs similarity index 89% rename from src/contrib/qr_code_scanner/camera_paintable.rs rename to src/contrib/qr_code_scanner/camera/camera_paintable/linux.rs index 7e94626e..22081891 100644 --- a/src/contrib/qr_code_scanner/camera_paintable.rs +++ b/src/contrib/qr_code_scanner/camera/camera_paintable/linux.rs @@ -24,53 +24,39 @@ use gtk::{ prelude::*, subclass::prelude::*, }; -use matrix_sdk::encryption::verification::QrVerificationData; use tracing::{debug, error}; +use super::{Action, CameraPaintable, CameraPaintableImpl}; use crate::contrib::qr_code_scanner::{qr_code_detector::QrCodeDetector, QrVerificationDataBoxed}; -pub enum Action { - QrCodeDetected(QrVerificationData), -} - mod imp { use std::cell::RefCell; - use glib::subclass; - use once_cell::sync::Lazy; - use super::*; #[derive(Debug, Default)] - pub struct CameraPaintable { + pub struct LinuxCameraPaintable { pub pipeline: RefCell>, pub sink_paintable: RefCell>, } #[glib::object_subclass] - impl ObjectSubclass for CameraPaintable { - const NAME: &'static str = "CameraPaintable"; - type Type = super::CameraPaintable; + impl ObjectSubclass for LinuxCameraPaintable { + const NAME: &'static str = "LinuxCameraPaintable"; + type Type = super::LinuxCameraPaintable; + type ParentType = CameraPaintable; type Interfaces = (gdk::Paintable,); } - impl ObjectImpl for CameraPaintable { + impl ObjectImpl for LinuxCameraPaintable { fn dispose(&self) { self.obj().set_pipeline(None); } - - fn signals() -> &'static [subclass::Signal] { - static SIGNALS: Lazy> = Lazy::new(|| { - vec![subclass::Signal::builder("code-detected") - .param_types([QrVerificationDataBoxed::static_type()]) - .run_first() - .build()] - }); - SIGNALS.as_ref() - } } - impl PaintableImpl for CameraPaintable { + impl CameraPaintableImpl for LinuxCameraPaintable {} + + impl PaintableImpl for LinuxCameraPaintable { fn intrinsic_height(&self) -> i32 { if let Some(paintable) = self.sink_paintable.borrow().as_ref() { paintable.intrinsic_height() @@ -122,10 +108,12 @@ mod imp { } glib::wrapper! { - pub struct CameraPaintable(ObjectSubclass) @implements gdk::Paintable; + /// A paintable to display the output of a camera on Linux. + pub struct LinuxCameraPaintable(ObjectSubclass) + @extends CameraPaintable, @implements gdk::Paintable; } -impl CameraPaintable { +impl LinuxCameraPaintable { pub async fn new(fd: F, streams: Vec) -> Self { let self_: Self = glib::Object::new(); diff --git a/src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs b/src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs new file mode 100644 index 00000000..6020828f --- /dev/null +++ b/src/contrib/qr_code_scanner/camera/camera_paintable/mod.rs @@ -0,0 +1,101 @@ +/// Subclassable camera paintable. +use gtk::{gdk, glib, glib::closure_local, prelude::*, subclass::prelude::*}; +use matrix_sdk::encryption::verification::QrVerificationData; + +#[cfg(target_os = "linux")] +pub mod linux; + +use crate::contrib::qr_code_scanner::QrVerificationDataBoxed; + +pub enum Action { + QrCodeDetected(QrVerificationData), +} + +mod imp { + use glib::subclass::Signal; + use once_cell::sync::Lazy; + + use super::*; + + #[repr(C)] + pub struct CameraPaintableClass { + pub parent_class: glib::object::Class, + } + + unsafe impl ClassStruct for CameraPaintableClass { + type Type = CameraPaintable; + } + + #[derive(Debug, Default)] + pub struct CameraPaintable; + + #[glib::object_subclass] + impl ObjectSubclass for CameraPaintable { + const NAME: &'static str = "CameraPaintable"; + type Type = super::CameraPaintable; + type Class = CameraPaintableClass; + type Interfaces = (gdk::Paintable,); + } + + impl ObjectImpl for CameraPaintable { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![Signal::builder("code-detected") + .param_types([QrVerificationDataBoxed::static_type()]) + .run_first() + .build()] + }); + SIGNALS.as_ref() + } + } + + impl PaintableImpl for CameraPaintable { + fn snapshot(&self, _snapshot: &gdk::Snapshot, _width: f64, _height: f64) { + // Nothing to do + } + } +} + +glib::wrapper! { + /// A subclassable paintable to display the output of a camera. + pub struct CameraPaintable(ObjectSubclass) + @implements gdk::Paintable; +} + +pub trait CameraPaintableExt: 'static { + /// Connect to the signal emitted when a code is detected. + fn connect_code_detected( + &self, + f: F, + ) -> glib::SignalHandlerId; +} + +impl> CameraPaintableExt for O { + fn connect_code_detected( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "activated", + true, + closure_local!(move |obj: Self, data: QrVerificationDataBoxed| { + f(&obj, data.0); + }), + ) + } +} + +/// Public trait that must be implemented for everything that derives from +/// `CameraPaintable`. +/// +/// Overriding a method from this Trait overrides also its behavior in +/// `CameraPaintableExt`. +#[allow(async_fn_in_trait)] +pub trait CameraPaintableImpl: ObjectImpl + PaintableImpl {} + +unsafe impl IsSubclassable for CameraPaintable +where + T: CameraPaintableImpl, + T::Type: IsA, +{ +} diff --git a/src/contrib/qr_code_scanner/camera/linux.rs b/src/contrib/qr_code_scanner/camera/linux.rs new file mode 100644 index 00000000..2378c560 --- /dev/null +++ b/src/contrib/qr_code_scanner/camera/linux.rs @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use std::time::Duration; + +use ashpd::desktop::camera; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use tracing::error; + +use super::{ + camera_paintable::{linux::LinuxCameraPaintable, CameraPaintable}, + Camera, CameraImpl, +}; +use crate::{spawn_tokio, utils::timeout_future}; + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct LinuxCamera { + pub paintable: glib::WeakRef, + } + + #[glib::object_subclass] + impl ObjectSubclass for LinuxCamera { + const NAME: &'static str = "LinuxCamera"; + type Type = super::LinuxCamera; + type ParentType = Camera; + } + + impl ObjectImpl for LinuxCamera {} + + impl CameraImpl for LinuxCamera { + async fn has_cameras(&self) -> bool { + let handle = spawn_tokio!(async move { + let camera = match camera::Camera::new().await { + Ok(camera) => camera, + Err(error) => { + error!("Failed to create instance of camera proxy: {error}"); + return false; + } + }; + + match camera.is_present().await { + Ok(is_present) => is_present, + Err(error) => { + error!("Failed to check whether system has cameras: {error}"); + false + } + } + }); + let abort_handle = handle.abort_handle(); + + match timeout_future(Duration::from_secs(1), handle).await { + Ok(is_present) => is_present.expect("The task should not have been aborted"), + Err(_) => { + abort_handle.abort(); + error!("Failed to check whether system has cameras: the request timed out"); + false + } + } + } + + async fn paintable(&self) -> Option { + // We need to make sure that the Paintable is taken only from the MainContext + assert!(glib::MainContext::default().is_owner()); + + if let Some(paintable) = self.paintable.upgrade() { + return Some(paintable.upcast()); + } + + let handle = spawn_tokio!(async move { camera::request().await }); + let abort_handle = handle.abort_handle(); + + match timeout_future(Duration::from_secs(1), handle).await { + Ok(tokio_res) => match tokio_res.expect("The task should not have been aborted") { + Ok(Some((fd, streams))) => { + let paintable = LinuxCameraPaintable::new(fd, streams).await; + self.paintable.set(Some(&paintable)); + + Some(paintable.upcast()) + } + Ok(None) => { + error!("Failed to request access to cameras: the response is empty"); + None + } + Err(error) => { + error!("Failed to request access to cameras: {error}"); + None + } + }, + Err(_) => { + // Error because we reached the timeout. + abort_handle.abort(); + error!("Failed to request access to cameras: the request timed out"); + None + } + } + } + } +} + +glib::wrapper! { + pub struct LinuxCamera(ObjectSubclass) @extends Camera; +} + +impl LinuxCamera { + /// Create a new `LinuxCamera`. + pub fn new() -> Self { + glib::Object::new() + } +} + +impl Default for LinuxCamera { + fn default() -> Self { + Self::new() + } +} + +unsafe impl Send for LinuxCamera {} +unsafe impl Sync for LinuxCamera {} diff --git a/src/contrib/qr_code_scanner/camera/mod.rs b/src/contrib/qr_code_scanner/camera/mod.rs new file mode 100644 index 00000000..4f22fa81 --- /dev/null +++ b/src/contrib/qr_code_scanner/camera/mod.rs @@ -0,0 +1,155 @@ +//! Camera API. + +use futures_util::{future::LocalBoxFuture, FutureExt}; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use once_cell::sync::Lazy; +use tracing::error; + +mod camera_paintable; +#[cfg(target_os = "linux")] +mod linux; + +pub use self::camera_paintable::Action; +use self::camera_paintable::CameraPaintable; + +mod imp { + + use super::*; + + #[repr(C)] + pub struct CameraClass { + pub parent_class: glib::object::Class, + pub has_cameras: fn(&super::Camera) -> LocalBoxFuture, + pub paintable: fn(&super::Camera) -> LocalBoxFuture>, + } + + unsafe impl ClassStruct for CameraClass { + type Type = Camera; + } + + pub(super) async fn camera_has_cameras(this: &super::Camera) -> bool { + let klass = this.class(); + (klass.as_ref().has_cameras)(this).await + } + + pub(super) async fn camera_paintable(this: &super::Camera) -> Option { + let klass = this.class(); + (klass.as_ref().paintable)(this).await + } + + #[derive(Debug, Default)] + pub struct Camera; + + #[glib::object_subclass] + impl ObjectSubclass for Camera { + const NAME: &'static str = "Camera"; + type Type = super::Camera; + type Class = CameraClass; + } + + impl ObjectImpl for Camera {} +} + +glib::wrapper! { + /// Subclassable Camera API. + /// + /// The default implementation, for unsupported platforms, makes sure the camera support is disabled. + pub struct Camera(ObjectSubclass); +} + +impl Camera { + /// Create a new `Camera`. + /// + /// Use `Camera::default()` to get a shared GObject. + fn new() -> Self { + #[cfg(target_os = "linux")] + let obj = linux::LinuxCamera::new().upcast(); + + #[cfg(not(target_os = "linux"))] + let obj = glib::Object::new(); + + obj + } +} + +impl Default for Camera { + fn default() -> Self { + static CAMERA: Lazy = Lazy::new(Camera::new); + + CAMERA.to_owned() + } +} + +unsafe impl Send for Camera {} +unsafe impl Sync for Camera {} + +pub trait CameraExt: 'static { + /// Whether any cameras are available. + async fn has_cameras(&self) -> bool; + + /// The paintable displaying the camera. + async fn paintable(&self) -> Option; +} + +impl> CameraExt for O { + async fn has_cameras(&self) -> bool { + imp::camera_has_cameras(self.upcast_ref()).await + } + + async fn paintable(&self) -> Option { + imp::camera_paintable(self.upcast_ref()).await + } +} + +/// Public trait that must be implemented for everything that derives from +/// `Camera`. +/// +/// Overriding a method from this Trait overrides also its behavior in +/// `CameraExt`. +#[allow(async_fn_in_trait)] +pub trait CameraImpl: ObjectImpl { + /// Whether any cameras are available. + async fn has_cameras(&self) -> bool { + false + } + + /// The paintable displaying the camera. + async fn paintable(&self) -> Option { + error!("The camera API is not supported on this platform"); + None + } +} + +unsafe impl IsSubclassable for Camera +where + T: CameraImpl, + T::Type: IsA, +{ + fn class_init(class: &mut glib::Class) { + Self::parent_class_init::(class.upcast_ref_mut()); + + let klass = class.as_mut(); + + klass.has_cameras = has_cameras_trampoline::; + klass.paintable = paintable_trampoline::; + } +} + +// Virtual method implementation trampolines. +fn has_cameras_trampoline(this: &Camera) -> LocalBoxFuture +where + T: ObjectSubclass + CameraImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().has_cameras().boxed_local() +} + +fn paintable_trampoline(this: &Camera) -> LocalBoxFuture> +where + T: ObjectSubclass + CameraImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().paintable().boxed_local() +} diff --git a/src/contrib/qr_code_scanner/mod.rs b/src/contrib/qr_code_scanner/mod.rs index ea91a8e4..612d6bc7 100644 --- a/src/contrib/qr_code_scanner/mod.rs +++ b/src/contrib/qr_code_scanner/mod.rs @@ -3,10 +3,9 @@ use gtk::{gdk, glib, glib::subclass, prelude::*, subclass::prelude::*}; use matrix_sdk::encryption::verification::QrVerificationData; mod camera; -mod camera_paintable; mod qr_code_detector; -pub use camera::Camera; +pub use camera::{Camera, CameraExt}; mod imp { use std::cell::RefCell; diff --git a/src/contrib/qr_code_scanner/qr_code_detector.rs b/src/contrib/qr_code_scanner/qr_code_detector.rs index 11537f0e..be84bf6c 100644 --- a/src/contrib/qr_code_scanner/qr_code_detector.rs +++ b/src/contrib/qr_code_scanner/qr_code_detector.rs @@ -8,7 +8,7 @@ use thiserror::Error; use tracing::debug; use super::*; -use crate::contrib::qr_code_scanner::camera_paintable::Action; +use crate::contrib::qr_code_scanner::camera::Action; const HEADER: &[u8] = b"MATRIX"; diff --git a/src/prelude.rs b/src/prelude.rs index 675d7d41..885479f9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +1,6 @@ pub use crate::{ components::{ToastableWindowExt, ToastableWindowImpl}, + contrib::CameraExt, session::model::{TimelineItemExt, UserExt}, session_list::SessionInfoExt, user_facing_error::UserFacingError, diff --git a/src/session/model/verification/mod.rs b/src/session/model/verification/mod.rs index cfb66252..0680f48d 100644 --- a/src/session/model/verification/mod.rs +++ b/src/session/model/verification/mod.rs @@ -10,7 +10,7 @@ pub use self::{ }, verification_list::VerificationList, }; -use crate::contrib::Camera; +use crate::{contrib::Camera, prelude::*}; /// A unique key to identify an identity verification. #[derive(Debug, Clone, Hash, PartialEq, Eq)]