[geary/mjog/user-plugins: 22/26] Rework and clean up how plugin extensions work



commit 154bb2d2c5e098968f961f478ed233e8b53e5924
Author: Michael Gratton <mike vee net>
Date:   Wed Mar 11 11:08:49 2020 +1100

    Rework and clean up how plugin extensions work
    
    Since libpeas doesn't allow a single plugin instance to have multiple
    extensions, define a single libpeas extension as a base class for all
    plugins and define extensions as non-libpeas interfaces shared by a
    single plugin. Manage loading/unloading these ourselves.
    
    This also defines a new trusted extension interface for plugins that
    need access to Geary's internals, new error domain for plugis, and
    made the notification context a plugin interface that is implemented by
    the application object.

 po/POTFILES.in                                     |  17 ++-
 .../application-notification-context.vala          | 164 +++------------------
 .../application/application-plugin-manager.vala    | 155 ++++++++++++-------
 src/client/meson.build                             |   6 +-
 .../desktop-notifications.vala                     |  35 +++--
 .../plugin/messaging-menu/messaging-menu.vala      |  10 +-
 .../notification-badge/notification-badge.vala     |  22 ++-
 src/client/plugin/plugin-error.vala                |  27 ++++
 .../plugin/plugin-notification-extension.vala      | 148 +++++++++++++++++++
 src/client/plugin/plugin-notification.vala         |  24 ---
 src/client/plugin/plugin-plugin-base.vala          |  36 +++++
 src/client/plugin/plugin-trusted-extension.vala    |  40 +++++
 12 files changed, 428 insertions(+), 256 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7f3178e5..10fdcaf5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -84,20 +84,23 @@ src/client/folder-list/folder-list-inboxes-branch.vala
 src/client/folder-list/folder-list-search-branch.vala
 src/client/folder-list/folder-list-special-grouping.vala
 src/client/folder-list/folder-list-tree.vala
-src/client/plugin/desktop-notifications/desktop-notifications.plugin.in
-src/client/plugin/desktop-notifications/desktop-notifications.vala
-src/client/plugin/messaging-menu/messaging-menu.plugin.in
-src/client/plugin/messaging-menu/messaging-menu.vala
-src/client/plugin/notification-badge/notification-badge.plugin.in
-src/client/plugin/notification-badge/notification-badge.vala
 src/client/plugin/plugin-account.vala
 src/client/plugin/plugin-application.vala
 src/client/plugin/plugin-contact-store.vala
 src/client/plugin/plugin-email-store.vala
 src/client/plugin/plugin-email.vala
+src/client/plugin/plugin-error.vala
 src/client/plugin/plugin-folder-store.vala
 src/client/plugin/plugin-folder.vala
-src/client/plugin/plugin-notification.vala
+src/client/plugin/plugin-notification-etension.vala
+src/client/plugin/plugin-plugin-base.vala
+src/client/plugin/plugin-trusted-etension.vala
+src/client/plugin/desktop-notifications/desktop-notifications.plugin.in
+src/client/plugin/desktop-notifications/desktop-notifications.vala
+src/client/plugin/messaging-menu/messaging-menu.plugin.in
+src/client/plugin/messaging-menu/messaging-menu.vala
+src/client/plugin/notification-badge/notification-badge.plugin.in
+src/client/plugin/notification-badge/notification-badge.vala
 src/client/sidebar/sidebar-branch.vala
 src/client/sidebar/sidebar-common.vala
 src/client/sidebar/sidebar-count-cell-renderer.vala
diff --git a/src/client/application/application-notification-context.vala 
b/src/client/application/application-notification-context.vala
index 2152243e..3c4c018a 100644
--- a/src/client/application/application-notification-context.vala
+++ b/src/client/application/application-notification-context.vala
@@ -7,51 +7,15 @@
  */
 
 /**
- * Provides a context for notification plugins.
- *
- * The context provides an interface for notification plugins to
- * interface with the Geary client application. Plugins that implement
- * the plugins will be passed an instance of this class as the
- * `context` property.
- *
- * Plugins should register folders they wish to monitor by calling
- * {@link start_monitoring_folder}. The context will then start
- * keeping track of email being delivered to the folder and being seen
- * in a main window updating {@link total_new_messages} and emitting
- * the {@link new_messages_arrived} and {@link new_messages_retired}
- * signals as appropriate.
- *
- * @see Plugin.NotificationPlugin
+ * Implementation of the notification extension context.
  */
-public class Application.NotificationContext : Geary.BaseObject {
+internal class Application.NotificationContext :
+    Geary.BaseObject, Plugin.NotificationContext {
 
 
     private const Geary.Email.Field REQUIRED_FIELDS  = FLAGS;
 
 
-    private class ApplicationImpl : Geary.BaseObject, Plugin.Application {
-
-
-        private Client backing;
-        private FolderStoreFactory folders;
-
-
-        public ApplicationImpl(Client backing,
-                               FolderStoreFactory folders) {
-            this.backing = backing;
-            this.folders = folders;
-        }
-
-        public override void show_folder(Plugin.Folder folder) {
-            Geary.Folder? target = this.folders.get_engine_folder(folder);
-            if (target != null) {
-                this.backing.show_folder.begin(target);
-            }
-        }
-
-    }
-
-
     private class EmailStoreImpl : Geary.BaseObject, Plugin.EmailStore {
 
 
@@ -248,23 +212,8 @@ public class Application.NotificationContext : Geary.BaseObject {
         }
     }
 
-    /**
-     * Returns the plugin application object.
-     *
-     * No special permissions are required to use access this.
-     */
-    public Plugin.Application plugin_application {
-        get; private set;
-    }
-
-    /**
-     * Current total new message count for all monitored folders.
-     *
-     * This is the sum of the the counts returned by {@link
-     * get_new_message_count} for all folders that are being monitored
-     * after a call to {@link start_monitoring_folder}.
-     */
-    public int total_new_messages { get; private set; default = 0; }
+    public int total_new_messages { get { return this._total_new_messages; } }
+    public int _total_new_messages = 0;
 
     private Gee.Map<Geary.Folder,MonitorInformation> folder_information =
         new Gee.HashMap<Geary.Folder,MonitorInformation>();
@@ -273,84 +222,28 @@ public class Application.NotificationContext : Geary.BaseObject {
     private FolderStoreFactory folders_factory;
     private Plugin.FolderStore folders;
     private EmailStoreImpl email;
-    private PluginManager.PluginFlags flags;
-
-
-    /**
-     * Emitted when new messages have been downloaded.
-     *
-     * This will only be emitted for folders that are being monitored
-     * by calling {@link start_monitoring_folder}.
-     */
-    public signal void new_messages_arrived(
-        Plugin.Folder parent,
-        int total,
-        Gee.Collection<Plugin.EmailIdentifier> added
-    );
-
-    /**
-     * Emitted when a folder has been cleared of new messages.
-     *
-     * This will only be emitted for folders that are being monitored
-     * after a call to {@link start_monitoring_folder}.
-     */
-    public signal void new_messages_retired(Plugin.Folder parent, int total);
 
 
-    /** Constructs a new context instance. */
     internal NotificationContext(Client application,
-                                 FolderStoreFactory folders_factory,
-                                 PluginManager.PluginFlags flags) {
+                                 FolderStoreFactory folders_factory) {
         this.application = application;
         this.folders_factory = folders_factory;
         this.folders = folders_factory.new_folder_store();
         this.email = new EmailStoreImpl(application);
-        this.flags = flags;
+    }
 
-        this.plugin_application = new ApplicationImpl(
-            application, folders_factory
-        );
+    public async Plugin.EmailStore get_email()
+        throws Plugin.Error.PERMISSION_DENIED {
+        return this.email;
     }
 
-    /**
-     * Returns a store to lookup folders for notifications.
-     *
-     * This method may prompt for permission before returning.
-     *
-     * @throws Geary.EngineError.PERMISSIONS if permission to access
-     * this resource was not given
-     */
     public async Plugin.FolderStore get_folders()
-        throws Geary.EngineError.PERMISSIONS {
+        throws Plugin.Error.PERMISSION_DENIED {
         return this.folders;
     }
 
-    /**
-     * Returns a store to lookup email for notifications.
-     *
-     * This method may prompt for permission before returning.
-     *
-     * @throws Geary.EngineError.PERMISSIONS if permission to access
-     * this resource was not given
-     */
-    public async Plugin.EmailStore get_email()
-        throws Geary.EngineError.PERMISSIONS {
-        return this.email;
-    }
-
-    /**
-     * Returns a store to lookup contacts for notifications.
-     *
-     * This method may prompt for permission before returning.
-     *
-     * @throws Geary.EngineError.NOT_FOUND if the given account does
-     * not exist
-     * @throws Geary.EngineError.PERMISSIONS if permission to access
-     * this resource was not given
-     */
     public async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
-        throws Geary.EngineError.NOT_FOUND,
-            Geary.EngineError.PERMISSIONS {
+        throws Plugin.Error.NOT_FOUND, Plugin.Error.PERMISSION_DENIED {
         Geary.Folder? folder = this.folders_factory.get_engine_folder(source);
         AccountContext? context = null;
         if (folder != null) {
@@ -359,30 +252,13 @@ public class Application.NotificationContext : Geary.BaseObject {
             );
         }
         if (context == null) {
-            throw new Geary.EngineError.NOT_FOUND(
+            throw new Plugin.Error.NOT_FOUND(
                 "No account for folder: %s", source.display_name
             );
         }
         return new ContactStoreImpl(context.contacts);
     }
 
-    /**
-     * Returns the client's application object.
-     *
-     * Only plugins that are trusted by the client will be provided
-     * access to the application instance.
-     *
-     * @throws Geary.EngineError.PERMISSIONS if permission to access
-     * this resource was not given
-     */
-    public Client get_client_application()
-        throws Geary.EngineError.PERMISSIONS {
-        if (!(PluginManager.PluginFlags.TRUSTED in this.flags)) {
-            throw new Geary.EngineError.PERMISSIONS("Plugin is not trusted");
-        }
-        return this.application;
-    }
-
     /**
      * Determines if notifications should be made for a specific folder.
      *
@@ -417,14 +293,14 @@ public class Application.NotificationContext : Geary.BaseObject {
      * folder by a call to {@link start_monitoring_folder}.
      */
     public int get_new_message_count(Plugin.Folder target)
-        throws Geary.EngineError.NOT_FOUND {
+        throws Plugin.Error.NOT_FOUND {
         Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
         MonitorInformation? info = null;
         if (folder != null) {
             info = folder_information.get(folder);
         }
         if (info == null) {
-            throw new Geary.EngineError.NOT_FOUND(
+            throw new Plugin.Error.NOT_FOUND(
                 "No such folder: %s", folder.path.to_string()
             );
         }
@@ -534,18 +410,19 @@ public class Application.NotificationContext : Geary.BaseObject {
         Plugin.Folder folder =
             this.folders_factory.get_plugin_folder(info.folder);
         if (arrived) {
-            this.total_new_messages += delta.size;
+            this._total_new_messages += delta.size;
             new_messages_arrived(
                 folder,
                 info.recent_ids.size,
                 this.email.get_plugin_ids(delta, info.folder.account.information)
             );
         } else {
-            this.total_new_messages -= delta.size;
+            this._total_new_messages -= delta.size;
             new_messages_retired(
                 folder, info.recent_ids.size
             );
         }
+        notify_property("total-new-messages");
     }
 
     private void remove_folder(Geary.Folder target) {
@@ -555,7 +432,10 @@ public class Application.NotificationContext : Geary.BaseObject {
             target.email_flags_changed.disconnect(on_email_flags_changed);
             target.email_removed.disconnect(on_email_removed);
 
-            this.total_new_messages -= info.recent_ids.size;
+            if (!info.recent_ids.is_empty) {
+                this._total_new_messages -= info.recent_ids.size;
+                notify_property("total-new-messages");
+            }
 
             this.folder_information.unset(target);
         }
diff --git a/src/client/application/application-plugin-manager.vala 
b/src/client/application/application-plugin-manager.vala
index 3f3bcba7..0299727c 100644
--- a/src/client/application/application-plugin-manager.vala
+++ b/src/client/application/application-plugin-manager.vala
@@ -11,18 +11,34 @@
 public class Application.PluginManager : GLib.Object {
 
 
-    // Plugins that will be loaded automatically and trusted with
-    // access to the application if they have been installed
-    private const string[] TRUSTED_MODULES = {
+    // Plugins that will be loaded automatically when the client
+    // application stats up
+    private const string[] AUTOLOAD_MODULES = {
         "desktop-notifications",
         "notification-badge",
     };
 
-    /** Flags assigned to a plugin by the manager. */
-    [Flags]
-    public enum PluginFlags {
-        /** If set, the plugin is in the set of trusted plugins. */
-        TRUSTED;
+
+    private class ApplicationImpl : Geary.BaseObject, Plugin.Application {
+
+
+        private Client backing;
+        private FolderStoreFactory folders;
+
+
+        public ApplicationImpl(Client backing,
+                               FolderStoreFactory folders) {
+            this.backing = backing;
+            this.folders = folders;
+        }
+
+        public override void show_folder(Plugin.Folder folder) {
+            Geary.Folder? target = this.folders.get_engine_folder(folder);
+            if (target != null) {
+                this.backing.show_folder.begin(target);
+            }
+        }
+
     }
 
 
@@ -33,9 +49,10 @@ public class Application.PluginManager : GLib.Object {
 
     private FolderStoreFactory folders_factory;
 
-    private Peas.ExtensionSet notification_extensions;
-    private Gee.Set<NotificationContext> notification_contexts =
-        new Gee.HashSet<NotificationContext>();
+    private Gee.Map<Peas.PluginInfo,Plugin.PluginBase> plugin_set =
+        new Gee.HashMap<Peas.PluginInfo,Plugin.PluginBase>();
+    private Gee.Map<Peas.PluginInfo,NotificationContext> notification_contexts =
+        new Gee.HashMap<Peas.PluginInfo,NotificationContext>();
 
 
     public PluginManager(Client application) throws GLib.Error {
@@ -46,40 +63,16 @@ public class Application.PluginManager : GLib.Object {
         this.trusted_path = application.get_app_plugins_dir().get_path();
         this.plugins.add_search_path(trusted_path, null);
 
-        this.notification_extensions = new Peas.ExtensionSet(
-            this.plugins,
-            typeof(Plugin.Notification)
-        );
-        this.notification_extensions.extension_added.connect((info, extension) => {
-                Plugin.Notification? plugin = extension as Plugin.Notification;
-                if (plugin != null) {
-                    var context = new NotificationContext(
-                        this.application,
-                        this.folders_factory,
-                        to_plugin_flags(info)
-                    );
-                    this.notification_contexts.add(context);
-                    plugin.notifications = context;
-                    plugin.activate();
-                }
-            });
-        this.notification_extensions.extension_removed.connect((info, extension) => {
-                Plugin.Notification? plugin = extension as Plugin.Notification;
-                if (plugin != null) {
-                    plugin.deactivate(this.is_shutdown);
-                }
-                var context = plugin.notifications;
-                context.destroy();
-                this.notification_contexts.remove(context);
-            });
+        this.plugins.load_plugin.connect_after(on_load_plugin);
+        this.plugins.unload_plugin.connect(on_unload_plugin);
 
         string[] optional_names = application.config.get_optional_plugins();
         foreach (Peas.PluginInfo info in this.plugins.get_plugin_list()) {
             string name = info.get_module_name();
             try {
                 if (info.is_available()) {
-                    if (is_trusted(info)) {
-                        debug("Loading trusted plugin: %s", name);
+                    if (is_autoload(info)) {
+                        debug("Loading autoload plugin: %s", name);
                         this.plugins.load_plugin(info);
                     } else if (name in optional_names) {
                         debug("Loading optional plugin: %s", name);
@@ -92,15 +85,9 @@ public class Application.PluginManager : GLib.Object {
         }
     }
 
-    public inline bool is_trusted(Peas.PluginInfo plugin) {
-        return (
-            plugin.get_module_name() in TRUSTED_MODULES &&
-            plugin.get_module_dir().has_prefix(trusted_path)
-        );
-    }
-
-    public inline PluginFlags to_plugin_flags(Peas.PluginInfo plugin) {
-        return is_trusted(plugin) ? PluginFlags.TRUSTED : 0;
+    /** Returns the engine folder for the given plugin folder, if any. */
+    public Geary.Folder? get_engine_folder(Plugin.Folder plugin) {
+        return this.folders_factory.get_engine_folder(plugin);
     }
 
     public Gee.Collection<Peas.PluginInfo> get_optional_plugins() {
@@ -108,7 +95,7 @@ public class Application.PluginManager : GLib.Object {
         foreach (Peas.PluginInfo plugin in this.plugins.get_plugin_list()) {
             try {
                 plugin.is_available();
-                if (!is_trusted(plugin)) {
+                if (!is_autoload(plugin)) {
                     plugins.add(plugin);
                 }
             } catch (GLib.Error err) {
@@ -125,7 +112,7 @@ public class Application.PluginManager : GLib.Object {
         bool loaded = false;
         if (plugin.is_available() &&
             !plugin.is_loaded() &&
-            !is_trusted(plugin)) {
+            !is_autoload(plugin)) {
             this.plugins.load_plugin(plugin);
             loaded = true;
             string name = plugin.get_module_name();
@@ -143,7 +130,7 @@ public class Application.PluginManager : GLib.Object {
         bool unloaded = false;
         if (plugin.is_available() &&
             plugin.is_loaded() &&
-            !is_trusted(plugin)) {
+            !is_autoload(plugin)) {
             this.plugins.unload_plugin(plugin);
             unloaded = true;
             string name = plugin.get_module_name();
@@ -163,11 +150,75 @@ public class Application.PluginManager : GLib.Object {
     internal void close() throws GLib.Error {
         this.is_shutdown = true;
         this.plugins.set_loaded_plugins(null);
+        this.plugins.garbage_collect();
         this.folders_factory.destroy();
     }
 
+    internal inline bool is_autoload(Peas.PluginInfo info) {
+        return info.get_module_name() in AUTOLOAD_MODULES;
+    }
+
     internal Gee.Collection<NotificationContext> get_notification_contexts() {
-        return this.notification_contexts.read_only_view;
+        return this.notification_contexts.values.read_only_view;
+    }
+
+    private void on_load_plugin(Peas.PluginInfo info) {
+        var plugin = this.plugins.create_extension(
+            info,
+            typeof(Plugin.PluginBase),
+            "plugin_application",
+            new ApplicationImpl(this.application, this.folders_factory)
+        ) as Plugin.PluginBase;
+        if (plugin != null) {
+            bool do_activate = true;
+            var trusted = plugin as Plugin.TrustedExtension;
+            if (trusted != null) {
+                if (info.get_module_dir().has_prefix(this.trusted_path)) {
+                    trusted.client_application = this.application;
+                    trusted.client_plugins = this;
+                } else {
+                    do_activate = false;
+                    this.plugins.unload_plugin(info);
+                }
+            }
+
+            var notification = plugin as Plugin.NotificationExtension;
+            if (notification != null) {
+                var context = new NotificationContext(
+                    this.application,
+                    this.folders_factory
+                );
+                this.notification_contexts.set(info, context);
+                notification.notifications = context;
+            }
+
+            if (do_activate) {
+                this.plugin_set.set(info, plugin);
+                plugin.activate();
+            }
+        } else {
+            warning(
+                "Could not construct BasePlugin from %s", info.get_module_name()
+            );
+        }
+    }
+
+    private void on_unload_plugin(Peas.PluginInfo info) {
+        var plugin = this.plugin_set.get(info);
+        if (plugin != null) {
+            plugin.deactivate(this.is_shutdown);
+
+            var notification = plugin as Plugin.NotificationExtension;
+            if (notification != null) {
+                var context = this.notification_contexts.get(info);
+                if (context != null) {
+                    this.notification_contexts.unset(info);
+                    context.destroy();
+                }
+            }
+
+            this.plugin_set.unset(info);
+        }
     }
 
 }
diff --git a/src/client/meson.build b/src/client/meson.build
index da3e81e5..d6dd9a4a 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -1,4 +1,5 @@
 # Geary client
+
 geary_client_vala_sources = files(
   'application/application-attachment-manager.vala',
   'application/application-avatar-store.vala',
@@ -97,9 +98,12 @@ geary_client_vala_sources = files(
   'plugin/plugin-contact-store.vala',
   'plugin/plugin-email-store.vala',
   'plugin/plugin-email.vala',
+  'plugin/plugin-error.vala',
   'plugin/plugin-folder-store.vala',
   'plugin/plugin-folder.vala',
-  'plugin/plugin-notification.vala',
+  'plugin/plugin-notification-extension.vala',
+  'plugin/plugin-plugin-base.vala',
+  'plugin/plugin-trusted-extension.vala',
 
   'sidebar/sidebar-branch.vala',
   'sidebar/sidebar-common.vala',
diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala 
b/src/client/plugin/desktop-notifications/desktop-notifications.vala
index 95c0c9bc..24769fc8 100644
--- a/src/client/plugin/desktop-notifications/desktop-notifications.vala
+++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala
@@ -10,7 +10,7 @@
 public void peas_register_types(TypeModule module) {
     Peas.ObjectModule obj = module as Peas.ObjectModule;
     obj.register_extension_type(
-        typeof(Plugin.Notification),
+        typeof(Plugin.PluginBase),
         typeof(Plugin.DesktopNotifications)
     );
 }
@@ -18,35 +18,34 @@ public void peas_register_types(TypeModule module) {
 /**
  * Manages standard desktop application notifications.
  */
-public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
+public class Plugin.DesktopNotifications :
+    PluginBase, NotificationExtension, TrustedExtension {
 
 
     private const Geary.SpecialFolderType[] MONITORED_TYPES = {
         INBOX, NONE
     };
 
-    public global::Application.NotificationContext notifications {
-        get; set;
+    public NotificationContext notifications {
+        get; set construct;
+    }
+
+    public global::Application.Client client_application {
+        get; set construct;
+    }
+
+    public global::Application.PluginManager client_plugins {
+        get; set construct;
     }
 
     private const string ARRIVED_ID = "email-arrived";
 
-    private global::Application.Client? application = null;
     private EmailStore? email = null;
     private GLib.Notification? arrived_notification = null;
     private GLib.Cancellable? cancellable = null;
 
 
     public override void activate() {
-        try {
-            this.application = this.notifications.get_client_application();
-        } catch (GLib.Error error) {
-            warning(
-                "Failed obtain application instance: %s",
-                error.message
-            );
-        }
-
         this.notifications.new_messages_arrived.connect(on_new_messages_arrived);
         this.cancellable = new GLib.Cancellable();
 
@@ -94,7 +93,7 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
     }
 
     private void clear_arrived_notification() {
-        this.application.withdraw_notification(ARRIVED_ID);
+        this.client_application.withdraw_notification(ARRIVED_ID);
         this.arrived_notification = null;
     }
 
@@ -206,8 +205,8 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
 
         // Do not show notification actions under Unity, it's
         // notifications daemon doesn't support them.
-        if (this.application.config.desktop_environment == UNITY) {
-            this.application.send_notification(id, notification);
+        if (this.client_application.config.desktop_environment == UNITY) {
+            this.client_application.send_notification(id, notification);
             return notification;
         } else {
             if (action != null) {
@@ -216,7 +215,7 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
                 );
             }
 
-            this.application.send_notification(id, notification);
+            this.client_application.send_notification(id, notification);
             return notification;
         }
     }
diff --git a/src/client/plugin/messaging-menu/messaging-menu.vala 
b/src/client/plugin/messaging-menu/messaging-menu.vala
index a97840cd..29f65fdd 100644
--- a/src/client/plugin/messaging-menu/messaging-menu.vala
+++ b/src/client/plugin/messaging-menu/messaging-menu.vala
@@ -10,17 +10,17 @@
 public void peas_register_types(TypeModule module) {
     Peas.ObjectModule obj = module as Peas.ObjectModule;
     obj.register_extension_type(
-        typeof(Plugin.Notification),
+        typeof(Plugin.PluginBase),
         typeof(Plugin.MessagingMenu)
     );
 }
 
 /** Updates the Unity messaging menu when new mail arrives. */
-public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
+public class Plugin.MessagingMenu : PluginBase, NotificationExtension {
 
 
-    public global::Application.NotificationContext notifications {
-        get; set;
+    public NotificationContext notifications {
+        get; set construct;
     }
 
 
@@ -101,7 +101,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
         if (this.folders != null) {
             foreach (Folder folder in this.folders.get_folders()) {
                 if (source_id == get_source_id(folder)) {
-                    this.notifications.plugin_application.show_folder(folder);
+                    this.plugin_application.show_folder(folder);
                     break;
                 }
             }
diff --git a/src/client/plugin/notification-badge/notification-badge.vala 
b/src/client/plugin/notification-badge/notification-badge.vala
index 661ad662..58438f5c 100644
--- a/src/client/plugin/notification-badge/notification-badge.vala
+++ b/src/client/plugin/notification-badge/notification-badge.vala
@@ -10,21 +10,30 @@
 public void peas_register_types(TypeModule module) {
     Peas.ObjectModule obj = module as Peas.ObjectModule;
     obj.register_extension_type(
-        typeof(Plugin.Notification),
+        typeof(Plugin.PluginBase),
         typeof(Plugin.NotificationBadge)
     );
 }
 
 /** Updates Unity application badge with total new message count. */
-public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
+public class Plugin.NotificationBadge :
+    PluginBase, NotificationExtension, TrustedExtension {
 
 
     private const Geary.SpecialFolderType[] MONITORED_TYPES = {
         INBOX, NONE
     };
 
-    public global::Application.NotificationContext notifications {
-        get; set;
+    public NotificationContext notifications {
+        get; set construct;
+    }
+
+    public global::Application.Client client_application {
+        get; set construct;
+    }
+
+    public global::Application.PluginManager client_plugins {
+        get; set construct;
     }
 
     private UnityLauncherEntry? entry = null;
@@ -32,9 +41,8 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
 
     public override void activate() {
         try {
-            var application = this.notifications.get_client_application();
-            var connection = application.get_dbus_connection();
-            var path = application.get_dbus_object_path();
+            var connection = this.client_application.get_dbus_connection();
+            var path = this.client_application.get_dbus_object_path();
             if (connection == null || path == null) {
                 throw new GLib.IOError.NOT_CONNECTED(
                     "Application does not have a DBus connection or path"
diff --git a/src/client/plugin/plugin-error.vala b/src/client/plugin/plugin-error.vala
new file mode 100644
index 00000000..67fae4b9
--- /dev/null
+++ b/src/client/plugin/plugin-error.vala
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * The base class for objects implementing a client plugin.
+ *
+ * To implement a new plugin, have it derive from this type and
+ * implement any additional extension interfaces (such as {@link
+ * NotificationExtension}) as required.
+ */
+
+/**
+ * Errors when plugins request resources from their contexts.
+ */
+public errordomain Plugin.Error {
+
+    /** Raised when access to a requested resource was denied. */
+    PERMISSION_DENIED,
+
+    /** Raised when a requested resource was not found. */
+    NOT_FOUND;
+
+}
diff --git a/src/client/plugin/plugin-notification-extension.vala 
b/src/client/plugin/plugin-notification-extension.vala
new file mode 100644
index 00000000..230e98c7
--- /dev/null
+++ b/src/client/plugin/plugin-notification-extension.vala
@@ -0,0 +1,148 @@
+/*
+ * Copyright © 2019-2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A plugin extension point for notifying of mail sending or arriving.
+ */
+public interface Plugin.NotificationExtension : PluginBase {
+
+    /**
+     * Context object for notifications.
+     *
+     * This will be set during (or just after) plugin construction,
+     * before {@link PluginBase.activate} is called.
+     */
+    public abstract NotificationContext notifications {
+        get; set construct;
+    }
+
+}
+
+
+// XXX this should be an inner interface of NotificationExtension, but
+// GNOME/vala#918 prevents that.
+
+/**
+ * Provides a context for notification plugins.
+ *
+ * The context provides an interface for notification plugins to
+ * interface with the Geary client application. Plugins that implement
+ * the plugins will be passed an instance of this class as the
+ * `context` property.
+ *
+ * Plugins should register folders they wish to monitor by calling
+ * {@link start_monitoring_folder}. The context will then start
+ * keeping track of email being delivered to the folder and being seen
+ * in a main window updating {@link total_new_messages} and emitting
+ * the {@link new_messages_arrived} and {@link new_messages_retired}
+ * signals as appropriate.
+ *
+ * @see Plugin.NotificationExtension.notifications
+ */
+public interface Plugin.NotificationContext : Geary.BaseObject {
+
+
+    /**
+     * Current total new message count for all monitored folders.
+     *
+     * This is the sum of the the counts returned by {@link
+     * get_new_message_count} for all folders that are being monitored
+     * after a call to {@link start_monitoring_folder}.
+     */
+    public abstract int total_new_messages { get; default = 0; }
+
+    /**
+     * Emitted when new messages have been downloaded.
+     *
+     * This will only be emitted for folders that are being monitored
+     * by calling {@link start_monitoring_folder}.
+     */
+    public signal void new_messages_arrived(
+        Plugin.Folder parent,
+        int total,
+        Gee.Collection<Plugin.EmailIdentifier> added
+    );
+
+    /**
+     * Emitted when a folder has been cleared of new messages.
+     *
+     * This will only be emitted for folders that are being monitored
+     * after a call to {@link start_monitoring_folder}.
+     */
+    public signal void new_messages_retired(Plugin.Folder parent, int total);
+
+
+    /**
+     * Returns a store to lookup email for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Error.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public abstract async Plugin.EmailStore get_email()
+        throws Error.PERMISSION_DENIED;
+
+    /**
+     * Returns a store to lookup folders for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Error.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public abstract async Plugin.FolderStore get_folders()
+        throws Error.PERMISSION_DENIED;
+
+    /**
+     * Returns a store to lookup contacts for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Error.NOT_FOUND if the given account does
+     * not exist
+     * @throws Error.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public abstract async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
+        throws Error.NOT_FOUND, Error.PERMISSION_DENIED;
+
+    /**
+     * Determines if notifications should be made for a specific folder.
+     *
+     * Notification plugins should call this to first before
+     * displaying a "new mail" notification for mail in a specific
+     * folder. It will return true for any monitored folder that is
+     * not currently visible in the currently focused main window, if
+     * any.
+     */
+    public abstract bool should_notify_new_messages(Plugin.Folder target);
+
+    /**
+     * Returns the new message count for a specific folder.
+     *
+     * The context must have already been requested to monitor the
+     * folder by a call to {@link start_monitoring_folder}.
+     */
+    public abstract int get_new_message_count(Plugin.Folder target)
+        throws Error.NOT_FOUND;
+
+    /**
+     * Starts monitoring a folder for new messages.
+     *
+     * Notification plugins should call this to start the context
+     * recording new messages for a specific folder.
+     */
+    public abstract void start_monitoring_folder(Plugin.Folder target);
+
+    /** Stops monitoring a folder for new messages. */
+    public abstract void stop_monitoring_folder(Plugin.Folder target);
+
+    /** Determines if a folder is curently being monitored. */
+    public abstract bool is_monitoring_folder(Plugin.Folder target);
+
+}
diff --git a/src/client/plugin/plugin-plugin-base.vala b/src/client/plugin/plugin-plugin-base.vala
new file mode 100644
index 00000000..c7f0c5b2
--- /dev/null
+++ b/src/client/plugin/plugin-plugin-base.vala
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * The base class for objects implementing a client plugin.
+ *
+ * To implement a new plugin, have it derive from this type and
+ * implement any additional extension interfaces (such as {@link
+ * NotificationExtension}) as required.
+ */
+public abstract class Plugin.PluginBase : Geary.BaseObject {
+
+    /**
+     * Returns an object for interacting with the client application.
+     *
+     * No special permissions are required to use access this
+     * resource.
+     *
+     * This will be set during (or just after) plugin construction,
+     * before {@link PluginBase.activate} is called.
+     */
+    public Plugin.Application plugin_application {
+        get; construct;
+    }
+
+    /** Invoked to activate the plugin, after loading. */
+    public abstract void activate();
+
+    /** Invoked to deactivate the plugin, prior to unloading */
+    public abstract void deactivate(bool is_shutdown);
+
+}
diff --git a/src/client/plugin/plugin-trusted-extension.vala b/src/client/plugin/plugin-trusted-extension.vala
new file mode 100644
index 00000000..fc0d9bd3
--- /dev/null
+++ b/src/client/plugin/plugin-trusted-extension.vala
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A plugin extension point for trusted plugins.
+ *
+ * In-tree plugins may implement this interface if they require access
+ * to the client application's internal machinery.
+ *
+ * Since the client application and engine objects have no API
+ * stability guarantee, Geary will refuse to load out-of-tree plugins
+ * that implement this extension point.
+ */
+public interface Plugin.TrustedExtension : PluginBase {
+
+    /**
+     * Client application object.
+     *
+     * This will be set during (or just after) plugin construction,
+     * before {@link PluginBase.activate} is called.
+     */
+    public abstract global::Application.Client client_application {
+        get; set construct;
+    }
+
+    /**
+     * Client plugin manager object.
+     *
+     * This will be set during (or just after) plugin construction,
+     * before {@link PluginBase.activate} is called.
+     */
+    public abstract global::Application.PluginManager client_plugins {
+        get; set construct;
+    }
+
+}



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