Skip to content

Commit

Permalink
Implement a dialog to ask for background perms
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joshuamegnauth54 committed Aug 4, 2024
1 parent 373abfd commit e99d456
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 23 deletions.
13 changes: 13 additions & 0 deletions cosmic-portal-config/src/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,17 @@ use serde::{Deserialize, Serialize};
pub struct Background {
/// App ID and allowed status
pub apps: HashMap<String, bool>,
/// 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,
}
6 changes: 6 additions & 0 deletions i18n/en/xdg_desktop_portal_cosmic.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
34 changes: 33 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -43,6 +45,8 @@ pub struct CosmicPortal {
pub prev_rectangle: Option<screenshot::Rect>,
pub wayland_helper: crate::wayland::WaylandHelper,

pub background_prompts: HashMap<window::Id, background::Args>,

pub outputs: Vec<OutputState>,
pub active_output: Option<WlOutput>,
}
Expand All @@ -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<background::PermissionResponse>,
},
ConfigSetScreenshot(config::screenshot::Screenshot),
/// Update config from external changes
ConfigSubUpdate(config::Config),
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
Expand All @@ -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(_)
Expand All @@ -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))
Expand Down Expand Up @@ -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) => {
Expand Down
218 changes: 196 additions & 22 deletions src/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,47 +26,79 @@ pub struct Background {

impl Background {
pub fn new(tx: Sender<subscription::Event>) -> 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<GetAppState> {
// #[zbus(connection)] connection: &zbus::Connection,
) -> fdo::Result<HashMap<String, AppStatus>> {
// 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<NotifyBackgroundResult> {
// 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::<NotifyBackgroundResult>::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",
Expand Down Expand Up @@ -89,22 +127,24 @@ impl Background {
&self,
app_id: String,
enable: bool,
commandline: Vec<String>,
flags: u32,
) -> PortalResponse<bool> {
log::debug!("[background] Autostart not implemented");
PortalResponse::Success(enable)
) -> fdo::Result<bool> {
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<String, AppStatus>,
}
// #[derive(Clone, Debug, zvariant::SerializeDict, zvariant::Type)]
// #[zvariant(signature = "a{sv}")]
// pub struct GetAppState {
// apps: HashMap<String, AppStatus>,
// }

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, zvariant::Type)]
#[zvariant(signature = "u")]
Expand Down Expand Up @@ -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<PortalResponse<NotifyBackgroundResult>>,
}

#[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<Msg> {
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<crate::app::Msg> {
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<crate::app::Msg> {
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,
}
})
}
}
}
Loading

0 comments on commit e99d456

Please sign in to comment.