[gnome-break-timer] Handle errors if background permissions are not granted



commit 5a45f86b90d7399a1bd2491f75644327e91490d6
Author: Dylan McCall <dylan dylanmccall ca>
Date:   Wed Nov 18 22:59:17 2020 -0800

    Handle errors if background permissions are not granted
    
    If the RequestBackground fails, we show an info bar with a button to
    open the Applications panel in org.gnome.ControlCenter. In addition,
    add some proactive permission checks to detect if the permission is
    granted (or removed) outside of the application.

 org.gnome.BreakTimer.json               |   1 +
 org.gnome.BreakTimer.local.json         |  85 ----------------------
 src/common/IFreedesktopApplication.vala |  25 +++++++
 src/common/IPortalRequest.vala          |  27 +++++++
 src/common/meson.build                  |   2 +
 src/settings/Application.vala           |  27 +++++++
 src/settings/BreakManager.vala          | 124 ++++++++++++++++++++++++++------
 src/settings/MainWindow.vala            | 123 ++++++++++++++++++++++++++++++-
 8 files changed, 306 insertions(+), 108 deletions(-)
---
diff --git a/org.gnome.BreakTimer.json b/org.gnome.BreakTimer.json
index 8a494f2..5d34d68 100644
--- a/org.gnome.BreakTimer.json
+++ b/org.gnome.BreakTimer.json
@@ -9,6 +9,7 @@
         "--socket=x11",
         "--socket=wayland",
         "--socket=pulseaudio",
+        "--talk-name=org.gnome.ControlCenter",
         "--talk-name=org.gnome.Shell",
         "--talk-name=org.gnome.Mutter.IdleMonitor",
         "--talk-name=org.gnome.ScreenSaver",
diff --git a/src/common/IFreedesktopApplication.vala b/src/common/IFreedesktopApplication.vala
new file mode 100644
index 0000000..704850c
--- /dev/null
+++ b/src/common/IFreedesktopApplication.vala
@@ -0,0 +1,25 @@
+/*
+ * This file is part of GNOME Break Timer.
+ *
+ * GNOME Break Timer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GNOME Break Timer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNOME Break Timer.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace BreakTimer.Common {
+
+[DBus (name = "org.freedesktop.Application")]
+public interface IFreedesktopApplication : GLib.Object {
+    public abstract void activate_action (string action_name, Variant[] parameter, GLib.HashTable<string, 
Variant> platform_data) throws GLib.DBusError, GLib.IOError;
+}
+
+}
diff --git a/src/common/IPortalRequest.vala b/src/common/IPortalRequest.vala
new file mode 100644
index 0000000..1522514
--- /dev/null
+++ b/src/common/IPortalRequest.vala
@@ -0,0 +1,27 @@
+/*
+ * This file is part of GNOME Break Timer.
+ *
+ * GNOME Break Timer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GNOME Break Timer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNOME Break Timer.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace BreakTimer.Common {
+
+[DBus (name = "org.freedesktop.portal.Request")]
+public interface IPortalRequest : GLib.Object {
+    public signal void response (uint32 response, GLib.HashTable<string, Variant> results);
+
+    public abstract void close () throws GLib.DBusError, GLib.IOError;
+}
+
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index acd652b..6a9cfa9 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -1,9 +1,11 @@
 common_sources = files(
     'IBreakTimer_TimerBreak.vala',
     'IBreakTimer.vala',
+    'IFreedesktopApplication.vala',
     'IGnomeScreenSaver.vala',
     'IMutterIdleMonitor.vala',
     'IPortalBackground.vala',
+    'IPortalRequest.vala',
     'ISessionStatus.vala',
     'NaturalTime.vala'
 )
diff --git a/src/settings/Application.vala b/src/settings/Application.vala
index f80565b..8999503 100644
--- a/src/settings/Application.vala
+++ b/src/settings/Application.vala
@@ -55,6 +55,7 @@ public class Application : Gtk.Application {
 
     private BreakManager break_manager;
     private MainWindow main_window;
+    private bool initial_focus = true;
 
     public Application () {
         GLib.Object (
@@ -117,6 +118,32 @@ public class Application : Gtk.Application {
         } catch (GLib.Error error) {
             GLib.error("Error initializing main_window: %s", error.message);
         }
+
+        this.main_window.window_state_event.connect (this.on_main_window_window_state_event);
+    }
+
+    private bool on_main_window_window_state_event (Gdk.EventWindowState event) {
+        bool focused = (
+            Gdk.WindowState.FOCUSED in event.changed_mask &&
+            Gdk.WindowState.FOCUSED in event.new_window_state
+        );
+
+        if (focused && this.initial_focus && this.break_manager.master_enabled) {
+            // We should always refresh permissions at startup if enabled. Wait
+            // for a moment after the main window is focused before doing this,
+            // because it may trigger a system dialog.
+            this.initial_focus = false;
+            GLib.Timeout.add (500, () => {
+                this.break_manager.refresh_permissions ();
+                return false;
+            });
+        } else if (focused && this.break_manager.permissions_error != NONE) {
+            // Refresh permissions on focus if there was an error, and, for
+            // example, we are returning from GNOME Settings
+            this.break_manager.refresh_permissions ();
+        }
+
+        return false;
     }
 
     private void delayed_start () {
diff --git a/src/settings/BreakManager.vala b/src/settings/BreakManager.vala
index f5ce425..b395f2b 100644
--- a/src/settings/BreakManager.vala
+++ b/src/settings/BreakManager.vala
@@ -37,13 +37,23 @@ public class BreakManager : GLib.Object {
     public string[] selected_break_ids { get; set; }
     public BreakType? foreground_break { get; private set; }
 
+    public PermissionsError permissions_error { get; private set; }
+
     private GLib.DBusConnection dbus_connection;
 
     private IPortalBackground? background_portal = null;
+    private IPortalRequest? background_request = null;
+    private GLib.ObjectPath? background_request_path = null;
 
     public signal void break_status_available ();
     public signal void status_changed ();
 
+    public enum PermissionsError {
+        NONE,
+        AUTOSTART_NOT_ALLOWED,
+        BACKGROUND_NOT_ALLOWED
+    }
+
     public BreakManager (Application application) {
         this.application = application;
 
@@ -53,6 +63,8 @@ public class BreakManager : GLib.Object {
         this.breaks.append(new MicroBreakType ());
         this.breaks.append(new RestBreakType ());
 
+        this.permissions_error = PermissionsError.NONE;
+
         this.settings.bind ("enabled", this, "master-enabled", SettingsBindFlags.DEFAULT);
         this.settings.bind ("selected-breaks", this, "selected-break-ids", SettingsBindFlags.DEFAULT);
 
@@ -95,28 +107,98 @@ public class BreakManager : GLib.Object {
             this.launch_break_timer_service ();
         }
 
-        if (this.background_portal != null) {
-            var options = new HashTable<string, GLib.Variant> (str_hash, str_equal);
-            var commandline = new GLib.Variant.strv ({"gnome-break-timer-daemon"});
-            options.insert ("autostart", this.master_enabled);
-            options.insert ("commandline", commandline);
-            // RequestBackground creates a desktop file with the same name as
-            // the flatpak, which happens to be the dbus name of the daemon
-            // (although it is not the dbus name of the settings application).
-            options.insert ("dbus-activatable", true);
-
-            try {
-                // We don't have a nice way to generate a window handle, but the
-                // background portal can probably do without.
-                // TODO: Handle response, and display an error if the result
-                //       includes `autostart == false || background == false`.
-                this.background_portal.request_background("", options);
-            } catch (GLib.IOError error) {
-                GLib.warning ("Error connecting to xdg desktop portal: %s", error.message);
-            } catch (GLib.DBusError error) {
-                GLib.warning ("Error enabling autostart: %s", error.message);
-            }
+        this.request_background (this.master_enabled);
+    }
+
+    public void refresh_permissions () {
+        if (this.master_enabled) {
+            this.request_background (this.master_enabled);
+        }
+    }
+
+    private bool request_background (bool autostart) {
+        if (this.background_portal == null) {
+            this.permissions_error = NONE;
+            return false;
+        }
+
+        string sender_name = this.dbus_connection.unique_name.replace(".", "_")[1:];
+        string handle_token = "org_gnome_breaktimer%d".printf(
+            GLib.Random.int_range(0, int.MAX)
+        );
+
+        var options = new HashTable<string, GLib.Variant> (str_hash, str_equal);
+        var commandline = new GLib.Variant.strv ({"gnome-break-timer-daemon"});
+        options.insert ("handle_token", handle_token);
+        options.insert ("autostart", autostart);
+        options.insert ("commandline", commandline);
+        // RequestBackground creates a desktop file with the same name as
+        // the flatpak, which happens to be the dbus name of the daemon
+        // (although it is not the dbus name of the settings application).
+        options.insert ("dbus-activatable", true);
+
+        GLib.ObjectPath request_path = null;
+        GLib.ObjectPath expected_request_path = new GLib.ObjectPath(
+            "/org/freedesktop/portal/desktop/request/%s/%s".printf(
+                sender_name,
+                handle_token
+            )
+        );
+
+        this.watch_background_request (expected_request_path);
+
+        try {
+            // We don't have a nice way to generate a window handle, but the
+            // background portal can probably do without.
+            // TODO: Handle response, and display an error if the result
+            //       includes `autostart == false || background == false`.
+            request_path = this.background_portal.request_background("", options);
+        } catch (GLib.IOError error) {
+            GLib.warning ("Error connecting to desktop portal: %s", error.message);
+            return false;
+        } catch (GLib.DBusError error) {
+            GLib.warning ("Error enabling autostart: %s", error.message);
+            return false;
+        }
+
+        this.watch_background_request (request_path);
+
+        return true;
+    }
+
+    private bool watch_background_request (GLib.ObjectPath request_path) {
+        if (request_path == this.background_request_path) {
+            return true;
         }
+
+        try {
+            this.background_request = this.dbus_connection.get_proxy_sync (
+                "org.freedesktop.portal.Desktop",
+                request_path
+            );
+            this.background_request_path = request_path;
+            this.background_request.response.connect (this.on_background_request_response);
+        } catch (GLib.IOError error) {
+            GLib.warning ("Error connecting to desktop portal: %s", error.message);
+            return false;
+        }
+
+        return true;
+    }
+
+    private void on_background_request_response (uint32 response, GLib.HashTable<string, Variant> results) {
+        bool background_allowed = (bool) results.get ("background");
+        bool autostart_allowed = (bool) results.get ("autostart");
+
+        if (this.master_enabled && ! autostart_allowed) {
+            this.permissions_error = AUTOSTART_NOT_ALLOWED;
+        } else if (this.master_enabled && ! background_allowed) {
+            this.permissions_error = BACKGROUND_NOT_ALLOWED;
+        } else {
+            this.permissions_error = NONE;
+        }
+
+        this.background_request = null;
     }
 
     private bool get_is_in_flatpak () {
diff --git a/src/settings/MainWindow.vala b/src/settings/MainWindow.vala
index a14a23c..1de741b 100644
--- a/src/settings/MainWindow.vala
+++ b/src/settings/MainWindow.vala
@@ -15,6 +15,7 @@
  * along with GNOME Break Timer.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+using BreakTimer.Common;
 using BreakTimer.Settings.Break;
 using BreakTimer.Settings.Panels;
 
@@ -25,10 +26,13 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
 
     private GLib.DBusConnection dbus_connection;
 
+    private GLib.HashTable<string, MessageBar> message_bars;
+
     private GLib.Menu app_menu;
 
     private Gtk.HeaderBar header;
     private Gtk.Stack main_stack;
+    private Gtk.Box messages_box;
 
     private Gtk.Button settings_button;
     private Gtk.Switch master_switch;
@@ -39,11 +43,60 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
     private WelcomePanel welcome_panel;
     private StatusPanel status_panel;
 
+    private class MessageBar : Gtk.InfoBar {
+        protected weak MainWindow main_window;
+
+        public signal void close_message_bar ();
+
+        protected MessageBar (MainWindow main_window) {
+            GLib.Object ();
+
+            this.main_window = main_window;
+        }
+    }
+
+    private class PermissionsErrorMessageBar : MessageBar {
+        private BreakManager.PermissionsError error_type;
+
+        public static int RESPONSE_OPEN_SETTINGS = 1;
+
+        public PermissionsErrorMessageBar (MainWindow main_window, BreakManager.PermissionsError error_type) 
{
+            base (main_window);
+
+            this.error_type = error_type;
+
+            this.add_button (_("Open Settings"), RESPONSE_OPEN_SETTINGS);
+
+            Gtk.Container content_area = this.get_content_area ();
+            Gtk.Label label = new Gtk.Label (_("Break Timer needs permission to start automatically and run 
in the background"));
+            content_area.add (label);
+
+            content_area.show_all ();
+
+            this.response.connect (this.on_response);
+            this.close.connect (this.on_close);
+        }
+
+        private void on_response (int response_id) {
+            if (response_id == RESPONSE_OPEN_SETTINGS) {
+                this.main_window.launch_application_settings ();
+            } else if (response_id == Gtk.ResponseType.CLOSE) {
+                this.close_message_bar ();
+            }
+        }
+
+        private void on_close () {
+            this.close_message_bar ();
+        }
+    }
+
     public MainWindow (Application application, BreakManager break_manager) {
         GLib.Object (application: application);
 
         this.break_manager = break_manager;
 
+        this.message_bars = new GLib.HashTable<string, MessageBar> (str_hash, str_equal);
+
         this.set_title ( _("Break Timer"));
         this.set_default_size (850, 400);
 
@@ -62,7 +115,7 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
         this.break_settings_dialog.set_modal (true);
         this.break_settings_dialog.set_transient_for (this);
 
-        Gtk.Grid content = new Gtk.Grid ();
+        Gtk.Box content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
         this.add (content);
         content.set_orientation (Gtk.Orientation.VERTICAL);
         content.set_vexpand (true);
@@ -93,8 +146,11 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
         settings_button.set_always_show_image (true);
         header.pack_end (this.settings_button);
 
+        this.messages_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        content.pack_end (this.messages_box);
+
         this.main_stack = new Gtk.Stack ();
-        content.add (this.main_stack);
+        content.pack_end (this.main_stack);
         main_stack.set_margin_top (6);
         main_stack.set_margin_bottom (6);
         main_stack.set_transition_duration (250);
@@ -109,6 +165,7 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
         this.header.show_all ();
         content.show_all ();
 
+        break_manager.notify["permissions-error"].connect (this.on_break_manager_permissions_error_change);
         break_manager.notify["foreground-break"].connect (this.update_visible_panel);
         this.update_visible_panel ();
     }
@@ -131,6 +188,38 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
         return true;
     }
 
+    private void on_break_manager_permissions_error_change () {
+        BreakManager.PermissionsError error_type = this.break_manager.permissions_error;
+        if (error_type == AUTOSTART_NOT_ALLOWED || error_type == BACKGROUND_NOT_ALLOWED) {
+            MessageBar message_bar = new PermissionsErrorMessageBar (this, error_type);
+            this.show_message_bar ("permissions-error", message_bar);
+        } else {
+            this.hide_message_bar ("permissions-error");
+        }
+    }
+
+    private void show_message_bar (string message_id, MessageBar message_bar) {
+        if (this.message_bars.contains (message_id)) {
+            return;
+        }
+
+        this.message_bars.set (message_id, message_bar);
+
+        this.messages_box.pack_end (message_bar);
+        message_bar.show ();
+        message_bar.close_message_bar.connect (() => {
+            this.hide_message_bar(message_id);
+        });
+    }
+
+    private void hide_message_bar (string message_id) {
+        MessageBar? message_bar = this.message_bars.get (message_id);
+        if (message_bar != null) {
+            this.messages_box.remove (message_bar);
+            this.message_bars.remove (message_id);
+        }
+    }
+
     public Gtk.Widget get_master_switch () {
         return this.master_switch;
     }
@@ -191,6 +280,36 @@ public class MainWindow : Gtk.ApplicationWindow, GLib.Initable {
         this.break_settings_dialog.show ();
         this.welcome_panel.settings_button_clicked ();
     }
+
+    private bool launch_application_settings () {
+        // Try to launch GNOME Settings pointing at the Applications panel.
+        // This feels kind of dirty and it would be nice if there was a better
+        // way.
+        // TODO: Can we pre-select org.gnome.BreakTimer?
+
+        GLib.Variant[] parameters = {
+            new GLib.Variant ("(sav)", "applications")
+        };
+        GLib.HashTable<string, Variant> platform_data = new GLib.HashTable<string, Variant> (str_hash, 
str_equal);
+
+        try {
+            IFreedesktopApplication control_center_application = this.dbus_connection.get_proxy_sync (
+                "org.gnome.ControlCenter",
+                "/org/gnome/ControlCenter",
+                GLib.DBusProxyFlags.DO_NOT_AUTO_START,
+                null
+            );
+            control_center_application.activate_action("launch-panel", parameters, platform_data);
+        } catch (GLib.IOError error) {
+            GLib.warning ("Error connecting to org.gnome.ControlCenter: %s", error.message);
+            return false;
+        } catch (GLib.DBusError error) {
+            GLib.warning ("Error launching org.gnome.ControlCenter: %s", error.message);
+            return false;
+        }
+
+        return true;
+    }
 }
 
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]