From e99d45619056001eeb0ef33dd788e4482b19b4a6 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Tue, 30 Jul 2024 02:01:53 -0400 Subject: [PATCH] Implement a dialog to ask for background perms The dialog asks the user if they wish to grant background permissions to the asking app. The dialog is optional according to the docs. GNOME skips it while KDE implements it. This implementation enables the dialog by default while also allowing the user to bypass it through the config. --- cosmic-portal-config/src/background.rs | 13 ++ i18n/en/xdg_desktop_portal_cosmic.ftl | 6 + src/app.rs | 34 +++- src/background.rs | 218 ++++++++++++++++++++++--- src/subscription.rs | 14 ++ 5 files changed, 262 insertions(+), 23 deletions(-) diff --git a/cosmic-portal-config/src/background.rs b/cosmic-portal-config/src/background.rs index c5bdb96..a096053 100644 --- a/cosmic-portal-config/src/background.rs +++ b/cosmic-portal-config/src/background.rs @@ -9,4 +9,17 @@ use serde::{Deserialize, Serialize}; pub struct Background { /// App ID and allowed status pub apps: HashMap, + /// Default preference for NotifyBackground's dialog + pub default_perm: PermissionDialog, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] +pub enum PermissionDialog { + /// Grant apps permission to run in the background + Allow, + /// Deny apps permission to run in the background + Deny, + /// Always ask if new apps should be granted background permissions + #[default] + Ask, } diff --git a/i18n/en/xdg_desktop_portal_cosmic.ftl b/i18n/en/xdg_desktop_portal_cosmic.ftl index 5e6fe12..3c1efbf 100644 --- a/i18n/en/xdg_desktop_portal_cosmic.ftl +++ b/i18n/en/xdg_desktop_portal_cosmic.ftl @@ -13,3 +13,9 @@ share-screen = Share your screen unknown-application = Unknown Application output = Output window = Window + +# Background portal +allow-once = Allow once +deny = Deny +bg-dialog-title = Background +bg-dialog-body = {$appname} requests to run in the background. This will allow it to run without any open windows. diff --git a/src/app.rs b/src/app.rs index 74a7829..58a11f4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,6 @@ -use crate::{access, config, file_chooser, fl, screencast_dialog, screenshot, subscription}; +use crate::{ + access, background, config, file_chooser, fl, screencast_dialog, screenshot, subscription, +}; use cosmic::iced_core::event::wayland::OutputEvent; use cosmic::widget::{self, dropdown}; use cosmic::Command; @@ -43,6 +45,8 @@ pub struct CosmicPortal { pub prev_rectangle: Option, pub wayland_helper: crate::wayland::WaylandHelper, + pub background_prompts: HashMap, + pub outputs: Vec, pub active_output: Option, } @@ -64,8 +68,13 @@ pub enum Msg { FileChooser(window::Id, file_chooser::Msg), Screenshot(screenshot::Msg), Screencast(screencast_dialog::Msg), + Background(background::Msg), Portal(subscription::Event), Output(OutputEvent, WlOutput), + ConfigUpdateBackground { + app_id: String, + choice: Option, + }, ConfigSetScreenshot(config::screenshot::Screenshot), /// Update config from external changes ConfigSubUpdate(config::Config), @@ -135,6 +144,7 @@ impl cosmic::Application for CosmicPortal { screencast_tab_model: Default::default(), location_options: Vec::new(), prev_rectangle: Default::default(), + background_prompts: Default::default(), outputs: Default::default(), active_output: Default::default(), wayland_helper, @@ -155,6 +165,8 @@ impl cosmic::Application for CosmicPortal { screencast_dialog::view(self).map(Msg::Screencast) } else if self.outputs.iter().any(|o| o.id == id) { screenshot::view(self, id).map(Msg::Screenshot) + } else if self.background_prompts.contains_key(&id) { + background::view(self, id).map(Msg::Background) } else { file_chooser::view(self, id) } @@ -181,6 +193,9 @@ impl cosmic::Application for CosmicPortal { subscription::Event::CancelScreencast(handle) => { screencast_dialog::cancel(self, handle).map(cosmic::app::Message::App) } + subscription::Event::Background(args) => { + background::update_args(self, args).map(cosmic::app::Message::App) + } subscription::Event::Config(config) => self.update(Msg::ConfigSubUpdate(config)), subscription::Event::Accent(_) | subscription::Event::IsDark(_) @@ -194,6 +209,7 @@ impl cosmic::Application for CosmicPortal { Msg::Screencast(m) => { screencast_dialog::update_msg(self, m).map(cosmic::app::Message::App) } + Msg::Background(m) => background::update_msg(self, m).map(cosmic::app::Message::App), Msg::Output(o_event, wl_output) => { match o_event { OutputEvent::Created(Some(info)) @@ -266,6 +282,22 @@ impl cosmic::Application for CosmicPortal { cosmic::iced::Command::none() } + Msg::ConfigUpdateBackground { app_id, choice } => { + if let (Some(choice), Some(handler)) = (choice, &mut self.config_handler) { + self.config + .background + .apps + .insert(app_id, choice == background::PermissionResponse::Allow); + if let Err(e) = self + .config + .set_background(handler, self.config.background.clone()) + { + log::error!("Failed to save background config: {e}"); + } + } + + cosmic::iced::Command::none() + } Msg::ConfigSetScreenshot(screenshot) => { match &mut self.config_handler { Some(handler) => { diff --git a/src/background.rs b/src/background.rs index 345b4e7..09450a1 100644 --- a/src/background.rs +++ b/src/background.rs @@ -2,10 +2,16 @@ use std::collections::{hash_map::Entry, HashMap}; +use cosmic::{iced::window, iced_runtime::command::Action, widget}; +use futures::{FutureExt, TryFutureExt}; use tokio::sync::mpsc::Sender; -use zbus::{object_server::SignalContext, zvariant}; +use zbus::{fdo, object_server::SignalContext, zvariant}; -use crate::{config, subscription, PortalResponse}; +use crate::{ + app::CosmicPortal, + config::{self, background::PermissionDialog}, + fl, subscription, PortalResponse, +}; const POP_SHELL_DEST: &str = "com.System76.PopShell"; const POP_SHELL_PATH: &str = "/com.System76.PopShell"; @@ -20,47 +26,79 @@ pub struct Background { impl Background { pub fn new(tx: Sender) -> Self { + // FIXME: Will need to change this to handle external updates (e.g. from cosmic-settings) let config = config::Config::load().0.background; Self { tx, config } } + + // fn dialog(name: &str) -> widget::Dialog<'_, PermissionResponse> {} } #[zbus::interface(name = "org.freedesktop.impl.portal.Background")] impl Background { - /// Get information on running apps + /// Current status on running apps async fn get_app_state( &self, - #[zbus(connection)] connection: &zbus::Connection, - // handle: zvariant::ObjectPath<'_>, - ) -> PortalResponse { + // #[zbus(connection)] connection: &zbus::Connection, + ) -> fdo::Result> { // TODO: How do I get running programs? log::warn!("[background] GetAppState is currently unimplemented"); - PortalResponse::Other + Ok(HashMap::default()) } + /// Notifies the user that an app is running in the background async fn notify_background( &mut self, - #[zbus(connection)] connection: &zbus::Connection, + // #[zbus(connection)] connection: &zbus::Connection, + #[zbus(signal_context)] context: SignalContext<'_>, handle: zvariant::ObjectPath<'_>, app_id: String, name: String, ) -> PortalResponse { - // Implementation notes log::debug!("[background] Request handle: {handle:?}"); match self.config.apps.entry(app_id) { + // Skip dialog based on default response set in configs + _ if self.config.default_perm == PermissionDialog::Allow => { + log::debug!("[background] AUTO ALLOW {name} based on default permission"); + PortalResponse::Success(NotifyBackgroundResult { + result: PermissionResponse::Allow, + }) + } + _ if self.config.default_perm == PermissionDialog::Deny => { + log::debug!("[background] AUTO DENY {name} based on default permission"); + PortalResponse::Success(NotifyBackgroundResult { + result: PermissionResponse::Deny, + }) + } + // Dialog Entry::Vacant(entry) => { log::debug!( "[background] Requesting permission for {} ({name})", entry.key() ); - // TODO: Dialog for user confirmation. - // For now, just allow all requests like GNOME - PortalResponse::Success(NotifyBackgroundResult { - result: PermissionResponse::Allow, - }) + let handle = handle.to_owned(); + let id = window::Id::unique(); + let app_id = entry.key().clone(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + self.tx + .send(subscription::Event::Background(Args { + handle, + id, + app_id, + tx, + })) + .inspect_err(|e| { + log::error!("[background] Failed to send message to register permissions dialog: {e:?}") + }) + .map_ok(|_| PortalResponse::::Other) + .map_err(|_| ()) + .and_then(|_| rx.recv().map(|out| out.ok_or(()))) + .unwrap_or_else(|_| PortalResponse::Other) + .await } + // We asked the user about this app already Entry::Occupied(entry) if *entry.get() => { log::debug!( "[background] AUTO ALLOW {} ({name}) based on cached response", @@ -89,22 +127,24 @@ impl Background { &self, app_id: String, enable: bool, + commandline: Vec, flags: u32, - ) -> PortalResponse { - log::debug!("[background] Autostart not implemented"); - PortalResponse::Success(enable) + ) -> fdo::Result { + log::warn!("[background] Autostart not implemented"); + Ok(enable) } + /// Emitted when running applications change their state #[zbus(signal)] pub async fn running_applications_changed(context: &SignalContext<'_>) -> zbus::Result<()>; } /// Information on running apps -#[derive(Clone, Debug, zvariant::SerializeDict, zvariant::Type)] -#[zvariant(signature = "a{sv}")] -pub struct GetAppState { - apps: HashMap, -} +// #[derive(Clone, Debug, zvariant::SerializeDict, zvariant::Type)] +// #[zvariant(signature = "a{sv}")] +// pub struct GetAppState { +// apps: HashMap, +// } #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, zvariant::Type)] #[zvariant(signature = "u")] @@ -135,3 +175,137 @@ pub enum PermissionResponse { /// Background permission allowed for a single instance AllowOnce, } + +#[derive(Clone)] +pub struct Args { + pub handle: zvariant::ObjectPath<'static>, + pub id: window::Id, + pub app_id: String, + tx: Sender>, +} + +#[derive(Debug, Clone)] +pub enum Msg { + Response { + id: window::Id, + choice: PermissionResponse, + }, + Cancel(window::Id), +} + +/// Permissions dialog +pub(crate) fn view(portal: &CosmicPortal, id: window::Id) -> cosmic::Element { + let name = portal + .background_prompts + .get(&id) + .map(|args| args.app_id.as_str()) + // xxx What do I do here? + .unwrap_or("Invalid window id"); + + // TODO: Add cancel + widget::dialog(fl!("bg-dialog-title")) + .body(fl!("bg-dialog-body", appname = name)) + .icon(widget::icon::from_name("dialog-warning-symbolic").size(64)) + .primary_action( + widget::button::suggested(fl!("allow")).on_press(Msg::Response { + id, + choice: PermissionResponse::Allow, + }), + ) + .secondary_action( + widget::button::suggested(fl!("allow-once")).on_press(Msg::Response { + id, + choice: PermissionResponse::AllowOnce, + }), + ) + .tertiary_action( + widget::button::destructive(fl!("deny")).on_press(Msg::Response { + id, + choice: PermissionResponse::Deny, + }), + ) + .into() +} + +/// Update Background dialog args for a specific window +pub fn update_args(portal: &mut CosmicPortal, args: Args) -> cosmic::Command { + if let Some(old) = portal.background_prompts.insert(args.id, args) { + // xxx Can this even happen? + log::trace!( + "[background] Replaced old dialog args for (window: {:?}) (app: {}) (handle: {})", + old.id, + old.app_id, + old.handle + ) + } + + cosmic::Command::none() +} + +pub fn update_msg(portal: &mut CosmicPortal, msg: Msg) -> cosmic::Command { + match msg { + Msg::Response { id, choice } => { + let Some(Args { + handle, + id, + app_id, + tx, + }) = portal.background_prompts.remove(&id) + else { + log::warn!("[background] Window {id:?} doesn't exist for some reason"); + return cosmic::Command::none(); + }; + + log::trace!( + "[background] User selected {choice:?} for (app: {app_id}) (handle: {handle})" + ); + // Return result to portal handler and update the config + cosmic::command::future(async move { + if let Err(e) = tx + .send(PortalResponse::Success(NotifyBackgroundResult { + result: choice, + })) + .await + { + log::error!("[background] Failed to send response from user to the background handler: {e:?}"); + } + + crate::app::Msg::ConfigUpdateBackground { + app_id, + choice: Some(choice), + } + }) + } + Msg::Cancel(id) => { + let Some(Args { + handle, + id, + app_id, + tx, + }) = portal.background_prompts.remove(&id) + else { + log::warn!("[background] Window {id:?} doesn't exist for some reason"); + return cosmic::Command::none(); + }; + + log::trace!( + "[background] User cancelled dialog for (window: {:?}) (app: {}) (handle: {})", + id, + app_id, + handle + ); + cosmic::command::future(async move { + if let Err(e) = tx.send(PortalResponse::Cancelled).await { + log::error!( + "[background] Failed to send cancellation response to background handler {e:?}" + ); + } + + crate::app::Msg::ConfigUpdateBackground { + app_id, + choice: None, + } + }) + } + } +} diff --git a/src/subscription.rs b/src/subscription.rs index 620a9ec..0cce15c 100644 --- a/src/subscription.rs +++ b/src/subscription.rs @@ -23,6 +23,7 @@ pub enum Event { Screenshot(crate::screenshot::Args), Screencast(crate::screencast_dialog::Args), CancelScreencast(zvariant::ObjectPath<'static>), + Background(crate::background::Args), Accent(Srgba), IsDark(bool), HighContrast(bool), @@ -76,6 +77,14 @@ impl Debug for Event { .finish(), Event::Screencast(s) => s.fmt(f), Event::CancelScreencast(h) => f.debug_tuple("CancelScreencast").field(h).finish(), + Event::Background(crate::background::Args { + handle, id, app_id, .. + }) => f + .debug_struct("Background") + .field("handle", handle) + .field("id", id) + .field("app_id", app_id) + .finish(), Event::Accent(a) => a.fmt(f), Event::IsDark(t) => t.fmt(f), Event::HighContrast(c) => c.fmt(f), @@ -180,6 +189,11 @@ pub(crate) async fn process_changes( log::error!("Error sending screencast cancel: {:?}", err); }; } + Event::Background(args) => { + if let Err(err) = output.send(Event::Background(args)).await { + log::error!("Error sending background event: {err:?}") + } + } Event::Accent(a) => { let object_server = conn.object_server(); let iface_ref = object_server.interface::<_, Settings>(DBUS_PATH).await?;