[epiphany/pgriffis/web-extension-alarms: 4/4] WebExtensions: Add complete implementation of alarms




commit 69cb5cba998f357f82df6049577090b071492ea6
Author: Patrick Griffis <pgriffis igalia com>
Date:   Fri May 27 13:19:09 2022 -0500

    WebExtensions: Add complete implementation of alarms

 .../resources/js/webextensions.js                  |   5 +
 src/webextension/api/alarms.c                      | 329 +++++++++++++++++++++
 src/webextension/api/alarms.h                      |  36 +++
 src/webextension/ephy-web-extension-manager.c      |  67 +++++
 src/webextension/meson.build                       |   1 +
 5 files changed, 438 insertions(+)
---
diff --git a/embed/web-process-extension/resources/js/webextensions.js 
b/embed/web-process-extension/resources/js/webextensions.js
index cc0dca1f8..dea92ae4b 100644
--- a/embed/web-process-extension/resources/js/webextensions.js
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -5,7 +5,12 @@
 
 // Browser async API
 window.browser.alarms = {
+    clear: function (...args) { return ephy_message ('alarms.clear', args); },
     clearAll: function (...args) { return ephy_message ('alarms.clearAll', args); },
+    create: function (...args) { return ephy_message ('alarms.create', args); },
+    get: function (...args) { return ephy_message ('alarms.get', args); },
+    getAll: function (...args) { return ephy_message ('alarms.getAll', args); },
+    onAlarm: new EphyEventListener (),
 };
 
 window.browser.windows = {
diff --git a/src/webextension/api/alarms.c b/src/webextension/api/alarms.c
new file mode 100644
index 000000000..a0b535d2c
--- /dev/null
+++ b/src/webextension/api/alarms.c
@@ -0,0 +1,329 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2019-2020 Jan-Michael Brummer <jan brummer tabos org>
+ *  Copyright © 2022 Igalia S.L.
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany 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.
+ *
+ *  Epiphany 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 Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <time.h>
+
+#include "ephy-embed-utils.h"
+#include "ephy-shell.h"
+#include "ephy-window.h"
+
+#include "storage.h"
+
+typedef struct {
+  EphyWebExtension *web_extension; /* Parent object */
+  char *name;
+  guint repeat_interval_ms;
+  double scheduled_time;
+  double repeat_interval_minutes;
+  int timeout_id;
+} Alarm;
+
+static void
+alarm_destroy (Alarm *alarm)
+{
+  g_clear_handle_id (&alarm->timeout_id, g_source_remove);
+  g_clear_pointer (&alarm->name, g_free);
+  g_free (alarm);
+}
+
+static GHashTable *
+get_alarms (EphyWebExtension *extension)
+{
+  GHashTable *alarms = g_object_get_data (G_OBJECT (extension), "alarms");
+  if (alarms)
+    return alarms;
+
+  alarms = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)alarm_destroy);
+  g_object_set_data_full (G_OBJECT (extension), "alarms", alarms, (GDestroyNotify)g_hash_table_destroy);
+  return alarms;
+}
+
+static gdouble
+get_double_property (JSCValue *object, const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (object, name);
+  if (jsc_value_is_number (value))
+    return jsc_value_to_double (value);
+  return 0.0;
+}
+
+static guint64
+time_now_ms (void)
+{
+  struct timespec spec;
+  clock_gettime (CLOCK_REALTIME, &spec);
+  return (spec.tv_sec * 1000) + (spec.tv_nsec / 1.0e6);
+}
+
+static guint
+timestamp_to_ms (double timestamp)
+{
+  guint64 now_ms = time_now_ms ();
+
+  if (now_ms > timestamp)
+    return 0;
+
+  return (guint)(timestamp - now_ms);
+}
+
+static guint
+minutes_to_ms (double minutes)
+{
+  return (guint)(minutes * 1.666667e-5);
+}
+
+static JsonNode *
+alarm_to_node (Alarm *alarm)
+{
+  JsonNode *node; 
+  JsonObject *obj;
+
+  if (!alarm)
+    return NULL;
+
+  node = json_node_init_object (json_node_alloc (), json_object_new ());
+  obj = json_node_get_object (node);
+  json_object_set_string_member (obj, "name", alarm->name);
+  json_object_set_double_member (obj, "scheduledTime", alarm->scheduled_time);
+  if (alarm->repeat_interval_ms)
+    json_object_set_double_member (obj, "periodInMinutes", alarm->repeat_interval_minutes);
+
+  return node;
+}
+
+static char *
+alarm_to_json (Alarm *alarm)
+{
+  g_autoptr (JsonNode) node = alarm_to_node (alarm); 
+  return json_to_string (node, FALSE);
+}
+
+static void
+emit_alarm (Alarm *alarm)
+{
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  g_autofree char *json = alarm_to_json (alarm);
+
+  ephy_web_extension_manager_emit_in_extension_views (manager, alarm->web_extension, "alarms.onAlarm", json);
+}
+
+static gboolean
+on_alarm_repeat (gpointer user_data)
+{
+  emit_alarm (user_data);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+on_alarm_start (gpointer user_data)
+{
+  GHashTable *alarms;
+  Alarm *alarm = user_data;
+  alarm->timeout_id = 0;
+
+  /* Remove, but don't free, ourselves before we call the extension. */
+  if (!alarm->repeat_interval_ms) {
+    alarms = get_alarms (alarm->web_extension);
+    g_hash_table_steal (alarms, alarm->name);
+  }
+
+  emit_alarm (alarm);
+
+  if (alarm->repeat_interval_ms) {
+    alarm->timeout_id = g_timeout_add (alarm->repeat_interval_ms, on_alarm_repeat, alarm);
+    alarm->scheduled_time = (double)(time_now_ms () + alarm->repeat_interval_ms);
+  } else {
+    alarm_destroy (alarm);
+  }
+
+  return G_SOURCE_REMOVE;
+}
+
+static char *
+alarms_handler_create (EphyWebExtension  *self,
+                       char              *name,
+                       JSCValue          *args,
+                       GError           **error)
+{
+  g_autoptr (JSCValue) alarm_name = NULL;
+  g_autoptr (JSCValue) alarm_info = NULL;
+  GHashTable *alarms = get_alarms (self);
+  Alarm *alarm;
+  double delay_in_minutes = 0.0;
+  double period_in_minutes = 0.0;
+  double when = 0.0;
+  g_autofree char *name_str = NULL;
+
+  /* This takes two optional args, name:str, info:obj */
+  alarm_name = jsc_value_object_get_property_at_index (args, 0);
+  if (jsc_value_is_string (alarm_name)) {
+    name_str = jsc_value_to_string (alarm_name);
+    alarm_info = jsc_value_object_get_property_at_index (args, 1);
+  } else {
+    name_str = g_strdup ("");
+    alarm_info = g_steal_pointer (&alarm_name);
+  }
+
+  if (jsc_value_is_object (alarm_info)) {
+    delay_in_minutes = get_double_property (alarm_info, "delayInMinutes");
+    period_in_minutes = get_double_property (alarm_info, "periodInMinutes");
+    when = get_double_property (alarm_info, "when");
+  }
+
+  if (delay_in_minutes && when) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "Both 'when' and 
'delayInMinutes' cannot be set");
+    return NULL;
+  }
+
+  alarm = g_new0 (Alarm, 1);
+  alarm->repeat_interval_ms = minutes_to_ms (period_in_minutes);
+  alarm->web_extension = self;
+  alarm->name = g_steal_pointer (&name_str);
+
+  if (delay_in_minutes) {
+    alarm->timeout_id = g_timeout_add (minutes_to_ms (delay_in_minutes), on_alarm_start, alarm);
+    alarm->scheduled_time = (double)(time_now_ms () + minutes_to_ms (delay_in_minutes));
+  } else if (when) {
+    alarm->timeout_id = g_timeout_add (timestamp_to_ms (when), on_alarm_start, alarm);
+    alarm->scheduled_time = when;
+  } else {
+    alarm->timeout_id = g_idle_add (on_alarm_start, alarm);
+    alarm->scheduled_time = (double)time_now_ms ();
+  }
+
+  g_hash_table_replace (alarms, alarm->name, alarm);
+
+  return NULL;
+}
+
+static char *
+alarms_handler_clear (EphyWebExtension  *self,
+                      char              *name,
+                      JSCValue          *args,
+                      GError           **error)
+{
+  GHashTable *alarms = get_alarms (self);
+  g_autoptr (JSCValue) name_value = jsc_value_object_get_property_at_index (args, 0);
+  g_autofree char *name_str = NULL;
+
+  if (!jsc_value_is_string (name_value))
+    name_str = g_strdup ("");
+  else
+    name_str = jsc_value_to_string (name_value);
+
+  if (g_hash_table_remove (alarms, name_str))
+    return g_strdup ("true");
+
+  return g_strdup ("false");
+}
+
+static char *
+alarms_handler_clear_all (EphyWebExtension  *self,
+                          char              *name,
+                          JSCValue          *args,
+                          GError           **error)
+{
+  GHashTable *alarms = get_alarms (self);
+
+  if (g_hash_table_size (alarms) == 0)
+    return g_strdup ("false");
+
+  g_hash_table_remove_all (alarms);
+  return g_strdup ("true");
+}
+
+static char *
+alarms_handler_get (EphyWebExtension  *self,
+                    char              *name,
+                    JSCValue          *args,
+                    GError           **error)
+{
+  GHashTable *alarms = get_alarms (self);
+  g_autoptr (JSCValue) name_value = jsc_value_object_get_property_at_index (args, 0);
+  g_autofree char *name_str = NULL;
+  Alarm *alarm;
+
+  if (!jsc_value_is_string (name_value))
+    name_str = g_strdup ("");
+  else
+    name_str = jsc_value_to_string (name_value);
+
+  alarm = g_hash_table_lookup (alarms, name_str);
+  return alarm_to_json (alarm);
+}
+
+static char *
+alarms_handler_get_all (EphyWebExtension  *self,
+                        char              *name,
+                        JSCValue          *args,
+                        GError           **error)
+{
+  GHashTable *alarms = get_alarms (self);
+  g_autoptr (JsonNode) node = json_node_init_array (json_node_alloc (), json_array_new ());
+  JsonArray *array = json_node_get_array (node);
+  GHashTableIter iter;
+  Alarm *alarm;
+
+  g_hash_table_iter_init (&iter, alarms);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer*)&alarm))
+    json_array_add_element (array, alarm_to_node (alarm));
+
+  return json_to_string (node, FALSE);
+}
+
+static EphyWebExtensionSyncApiHandler alarms_handlers[] = {
+  {"clear", alarms_handler_clear},
+  {"clearAll", alarms_handler_clear_all},
+  {"create", alarms_handler_create},
+  {"get", alarms_handler_get},
+  {"getAll", alarms_handler_get_all},
+};
+
+void
+ephy_web_extension_api_alarms_handler (EphyWebExtension *self,
+                                       char             *name,
+                                       JSCValue         *args,
+                                       GTask            *task)
+{
+  g_autoptr (GError) error = NULL;
+
+  for (guint idx = 0; idx < G_N_ELEMENTS (alarms_handlers); idx++) {
+    EphyWebExtensionSyncApiHandler handler = alarms_handlers[idx];
+    char *ret;
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      ret = handler.execute (self, name, args, &error);
+
+      if (error)
+        g_task_return_error (task, g_steal_pointer (&error));
+      else
+        g_task_return_pointer (task, ret, g_free);
+
+      return;
+    }
+  }
+
+  error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "Not Implemented");
+  g_task_return_error (task, g_steal_pointer (&error));
+}
diff --git a/src/webextension/api/alarms.h b/src/webextension/api/alarms.h
new file mode 100644
index 000000000..064ce7cef
--- /dev/null
+++ b/src/webextension/api/alarms.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2019-2020 Jan-Michael Brummer <jan brummer tabos org>
+ *  Copyright © 2022 Igalia S.L.
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany 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.
+ *
+ *  Epiphany 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 Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-web-extension.h"
+
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+void ephy_web_extension_api_alarms_handler (EphyWebExtension *self,
+                                            char            *name,
+                                            JSCValue        *value,
+                                            GTask           *task);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index e30b2eee9..7aa9409c8 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -35,6 +35,7 @@
 #include "ephy-web-extension-manager.h"
 #include "ephy-web-view.h"
 
+#include "api/alarms.h"
 #include "api/notifications.h"
 #include "api/pageaction.h"
 #include "api/runtime.h"
@@ -50,12 +51,15 @@ struct _EphyWebExtensionManager {
   GList *web_extensions;
   GHashTable *page_action_map;
   GHashTable *browser_action_map;
+
   GHashTable *background_web_views;
+  GHashTable *popup_web_views;
 };
 
 G_DEFINE_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, G_TYPE_OBJECT)
 
 EphyWebExtensionAsyncApiHandler api_handlers[] = {
+  {"alarms", ephy_web_extension_api_alarms_handler},
   {"notifications", ephy_web_extension_api_notifications_handler},
   {"pageAction", ephy_web_extension_api_pageaction_handler},
   {"runtime", ephy_web_extension_api_runtime_handler},
@@ -202,6 +206,7 @@ ephy_web_extension_manager_constructed (GObject *object)
   g_autofree char *dir = g_build_filename (ephy_default_profile_dir (), "web_extensions", NULL);
 
   self->background_web_views = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
(GDestroyNotify)gtk_widget_destroy);
+  self->popup_web_views = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
(GDestroyNotify)g_ptr_array_free);
   self->page_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_hash_table_destroy);
   self->browser_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)destroy_widget_list);
   self->web_extensions = NULL;
@@ -215,6 +220,7 @@ ephy_web_extension_manager_dispose (GObject *object)
   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (object);
 
   g_clear_pointer (&self->background_web_views, g_hash_table_destroy);
+  g_clear_pointer (&self->popup_web_views, g_hash_table_destroy);
   g_clear_pointer (&self->page_action_map, g_hash_table_destroy);
   g_list_free_full (g_steal_pointer (&self->web_extensions), g_object_unref);
 }
@@ -776,9 +782,37 @@ on_popup_load_changed (WebKitWebView   *web_view,
     gtk_widget_show (GTK_WIDGET (web_view));
 }
 
+static void
+on_popup_view_destroyed (GtkWidget *widget,
+                         gpointer   user_data)
+{
+  EphyWebExtension *web_extension = user_data;
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  GPtrArray *popup_views = g_hash_table_lookup (manager->popup_web_views, web_extension);
+
+  g_assert (g_ptr_array_remove_fast (popup_views, widget));
+}
+
+static void
+ephy_web_extension_manager_register_popup_view (EphyWebExtensionManager *manager,
+                                                EphyWebExtension        *web_extension,
+                                                GtkWidget               *web_view)
+{
+  GPtrArray *popup_views = g_hash_table_lookup (manager->popup_web_views, web_extension);
+
+  if (!popup_views) {
+    popup_views = g_ptr_array_new ();
+    g_hash_table_insert (manager->popup_web_views, web_extension, popup_views);
+  }
+
+  g_ptr_array_add (popup_views, web_view);
+  g_signal_connect (web_view, "destroy", G_CALLBACK (on_popup_view_destroyed), web_extension);
+}
+
 static GtkWidget *
 create_browser_popup (EphyWebExtension *web_extension)
 {
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
   GtkWidget *web_view;
   g_autofree char *data = NULL;
   g_autofree char *base_uri = NULL;
@@ -791,6 +825,8 @@ create_browser_popup (EphyWebExtension *web_extension)
   web_view = create_web_extensions_webview (web_extension);
   gtk_widget_hide (web_view); /* Shown in on_popup_load_changed. */
 
+  ephy_web_extension_manager_register_popup_view (manager, web_extension, web_view);
+
   popup = ephy_web_extension_get_browser_popup (web_extension);
   base_uri = create_base_uri_for_resource_path (web_extension, popup);
   data = ephy_web_extension_get_resource_as_string (web_extension, popup);
@@ -1087,6 +1123,7 @@ ephy_web_extension_manager_set_active (EphyWebExtensionManager *self,
   } else {
     g_hash_table_remove (self->browser_action_map, web_extension);
     g_hash_table_remove (self->background_web_views, web_extension);
+    g_object_set_data (G_OBJECT (web_extension), "alarms", NULL); /* Set in alarms.c */
   }
 }
 
@@ -1104,3 +1141,33 @@ ephy_web_extension_manager_get_page_action (EphyWebExtensionManager *self,
 
   return ret;
 }
+
+void
+ephy_web_extension_manager_emit_in_extension_views (EphyWebExtensionManager *self,
+                                                    EphyWebExtension        *web_extension,
+                                                    const char              *name,
+                                                    const char              *json)
+{
+  WebKitWebView *background_view = ephy_web_extension_manager_get_background_web_view (self, web_extension);
+  GPtrArray *popup_views = g_hash_table_lookup (self->popup_web_views, web_extension);
+  g_autofree char *script = g_strdup_printf ("window.browser.%s._emit(%s);", name, json);
+
+  if (background_view) {
+    webkit_web_view_run_javascript (background_view,
+                                    script,
+                                    NULL,
+                                    NULL,
+                                    NULL);
+  }
+
+  if (popup_views) {
+    for (guint i = 0; i < popup_views->len; i++) {
+      WebKitWebView *popup_view = g_ptr_array_index (popup_views, i);
+        webkit_web_view_run_javascript (popup_view,
+                                        script,
+                                        NULL,
+                                        NULL,
+                                        NULL);
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
index 5656e0b6e..671ded29b 100644
--- a/src/webextension/meson.build
+++ b/src/webextension/meson.build
@@ -1,4 +1,5 @@
 ephywebextension_src = [
+  'webextension/api/alarms.c',
   'webextension/api/notifications.c',
   'webextension/api/pageaction.c',
   'webextension/api/runtime.c',


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