[gnome-shell/eos3.8: 163/255] payg: Initial implementation of the PAYG manager component
- From: Matthew Leeds <mwleeds src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell/eos3.8: 163/255] payg: Initial implementation of the PAYG manager component
- Date: Wed, 10 Jun 2020 19:11:41 +0000 (UTC)
commit 7d4512ea542eed6219c97fc6e0afb684a4f0c47a
Author: Mario Sanchez Prada <mario endlessm com>
Date: Sat Mar 17 08:49:10 2018 +0000
payg: Initial implementation of the PAYG manager component
It simply proxies all the calls, signals and properties changes
between the shell and the eos-payg daemon.
https://phabricator.endlessm.com/T21607
* 2020-03-27:
+ Squash with ae2b6ee87
+ Squash with 7ae9e98d1
+ Squash with b16f98190
+ Squash with 1a41973e5
+ Squash with 355602f11
* 2019-03-13: Squashed with 544299762
payg: Fix the signature of the com.endlessm.Payg1.AddCode method
It used to expect an array of bytes (ay) but it now expects a string.
https://phabricator.endlessm.com/T21608
* 2019-10-21:
- Move D-Bus definition to XML file.
- Squash with "79487c5a6 payg: Autostart the eos-payg daemon […]"
- Squash with "7b26922ef payg: Add an isLocked attribute […]"
- Squash with "8b981d917 payg: Handle D-Bus errors […]"
- Squash with "dba82fb32 payg: Support the new property […]"
- Squash with "a8b64cf01 paygManager: support validating […]"
data/dbus-interfaces/com.endlessm.Payg1.xml | 13 +
data/gnome-shell-dbus-interfaces.gresource.xml | 1 +
js/js-resources.gresource.xml | 1 +
js/misc/paygManager.js | 410 +++++++++++++++++++++++++
js/ui/main.js | 7 +
po/POTFILES.in | 1 +
src/shell-util.c | 24 ++
src/shell-util.h | 2 +
8 files changed, 459 insertions(+)
---
diff --git a/data/dbus-interfaces/com.endlessm.Payg1.xml b/data/dbus-interfaces/com.endlessm.Payg1.xml
new file mode 100644
index 0000000000..7bd7d8d739
--- /dev/null
+++ b/data/dbus-interfaces/com.endlessm.Payg1.xml
@@ -0,0 +1,13 @@
+<node>
+ <interface name="com.endlessm.Payg1">
+ <method name="AddCode">
+ <arg type="s" direction="in" name="code"/>
+ </method>
+ <method name="ClearCode" />
+ <signal name="Expired" />
+ <property name="ExpiryTime" type="t" access="read"/>
+ <property name="Enabled" type="b" access="read"/>
+ <property name="RateLimitEndTime" type="t" access="read"/>
+ <property name="CodeFormat" type="s" access="read"/>
+ </interface>
+</node>
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index df3b64b074..28218ca293 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -4,6 +4,7 @@
<file preprocess="xml-stripblanks">com.endlessm.AppStore.xml</file>
<file preprocess="xml-stripblanks">com.endlessm.DiscoveryFeed.xml</file>
<file preprocess="xml-stripblanks">com.endlessm.DownloadManager1.Scheduler.xml</file>
+ <file preprocess="xml-stripblanks">com.endlessm.Payg1.xml</file>
<file preprocess="xml-stripblanks">com.endlessm.Speedwagon.xml</file>
<file preprocess="xml-stripblanks">net.hadess.SensorProxy.xml</file>
<file preprocess="xml-stripblanks">net.hadess.SwitcherooControl.xml</file>
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index d4e7915888..9c284a75ef 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -142,6 +142,7 @@
<!-- Endless-specific files beyond this point -->
<file>misc/parentalControlsManager.js</file>
+ <file>misc/paygManager.js</file>
<file>ui/appActivation.js</file>
<file>ui/appIconBar.js</file>
<file>ui/components/appStore.js</file>
diff --git a/js/misc/paygManager.js b/js/misc/paygManager.js
new file mode 100644
index 0000000000..49d236c247
--- /dev/null
+++ b/js/misc/paygManager.js
@@ -0,0 +1,410 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// Copyright (C) 2018 Endless Mobile, Inc.
+//
+// This is a GNOME Shell component to wrap the interactions over
+// D-Bus with the eos-payg system daemon.
+//
+// Licensed under the GNU General Public License Version 2
+//
+// This program 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 2
+// of the License, or (at your option) any later version.
+//
+// This program 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 this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+/* exported PaygManager */
+
+const { Gio, GLib, GnomeDesktop, GObject, Shell } = imports.gi;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const Gettext = imports.gettext;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+
+const EOS_PAYG_NAME = 'com.endlessm.Payg1';
+const EOS_PAYG_PATH = '/com/endlessm/Payg1';
+
+const EOS_PAYG_IFACE = loadInterfaceXML('com.endlessm.Payg1');
+
+var PaygErrorDomain = GLib.quark_from_string('payg-error');
+
+var PaygError = {
+ INVALID_CODE: 0,
+ CODE_ALREADY_USED: 1,
+ TOO_MANY_ATTEMPTS: 2,
+ DISABLED: 3,
+};
+
+const DBusErrorsMapping = {
+ INVALID_CODE: 'com.endlessm.Payg1.Error.InvalidCode',
+ CODE_ALREADY_USED: 'com.endlessm.Payg1.Error.CodeAlreadyUsed',
+ TOO_MANY_ATTEMPTS: 'com.endlessm.Payg1.Error.TooManyAttempts',
+ DISABLED: 'com.endlessm.Payg1.Error.Disabled',
+};
+
+// Title and description text to be shown in the periodic reminders.
+const NOTIFICATION_TITLE_TEXT = _('Pay as You Go');
+const NOTIFICATION_DETAILED_FORMAT_STRING = _('Subscription runs out in %s.');
+
+// This list defines the different instants in time where we would
+// want to show notifications to the user reminding that the payg
+// subscription will be expiring soon, up to a max GLib.MAXUINT32.
+//
+// It contains a list of integers representing the number of seconds
+// earlier to the expiration time when we want to show a notification,
+// which needs to be sorted in DESCENDING order.
+const notificationAlertTimesSecs = [
+ 60 * 60 * 48, // 2 days
+ 60 * 60 * 24, // 1 day
+ 60 * 60 * 2, // 2 hours
+ 60 * 60, // 1 hour
+ 60 * 30, // 30 minutes
+ 60 * 2, // 2 minutes
+ 30, // 30 seconds
+];
+
+// Takes an UNIX timestamp (in seconds) and returns a string
+// with a precision level appropriate to show to the user.
+//
+// The returned string will be formatted just in seconds for times
+// under 1 minute, in minutes for times under 2 hours, in hours and
+// minutes (if applicable) for times under 1 day, and then in days
+// and hours (if applicable) for anything longer than that in days.
+//
+// Some examples:
+// - 45 seconds => "45 seconds"
+// - 60 seconds => "1 minute"
+// - 95 seconds => "1 minute"
+// - 120 seconds => "2 minutes"
+// - 3600 seconds => "60 minutes"
+// - 4500 seconds => "75 minutes"
+// - 7200 seconds => "2 hours"
+// - 8640 seconds => "2 hours 24 minutes"
+// - 86400 seconds => "1 day"
+// - 115200 seconds => "1 day 8 hours"
+// - 172800 seconds => "2 days"
+function timeToString(seconds) {
+ if (seconds < 60)
+ return Gettext.ngettext('%s second', '%s seconds', seconds).format(Math.floor(seconds));
+
+ let minutes = Math.floor(seconds / 60);
+ if (minutes < 120)
+ return Gettext.ngettext('%s minute', '%s minutes', minutes).format(minutes);
+
+ let hours = Math.floor(minutes / 60);
+ if (hours < 24) {
+ let hoursStr = Gettext.ngettext('%s hour', '%s hours', hours).format(hours);
+
+ let minutesPast = minutes % 60;
+ if (minutesPast == 0)
+ return hoursStr;
+
+ let minutesStr = Gettext.ngettext('%s minute', '%s minutes', minutesPast).format(minutesPast);
+ return ("%s %s").format(hoursStr, minutesStr);
+ }
+
+ let days = Math.floor(hours / 24);
+ let daysStr = Gettext.ngettext('%s day', '%s days', days).format(days);
+
+ let hoursPast = hours % 24;
+ if (hoursPast == 0)
+ return daysStr;
+
+ let hoursStr = Gettext.ngettext('%s hour', '%s hours', hoursPast).format(hoursPast);
+ return ("%s %s").format(daysStr, hoursStr);
+}
+
+var PaygManager = GObject.registerClass({
+ Signals: {
+ 'code-expired': {},
+ 'code-format-changed': {},
+ 'enabled-changed': { param_types: [GObject.TYPE_BOOLEAN] },
+ 'expiry-time-changed': { param_types: [GObject.TYPE_INT64] },
+ 'initialized': {},
+ },
+}, class PaygManager extends GObject.Object {
+
+ _init() {
+ super._init();
+
+ this._initialized = false;
+ this._proxy = null;
+
+ this._enabled = false;
+ this._expiryTime = 0;
+ this._rateLimitEndTime = 0;
+ this._codeFormat = '';
+ this._codeFormatRegex = null;
+ this._notification = null;
+
+ // Keep track of clock changes to update notifications.
+ this._wallClock = new GnomeDesktop.WallClock({ time_only: true });
+ this._wallClock.connect('notify::clock', this._clockUpdated.bind(this));
+
+ // D-Bus related initialization code only below this point.
+ this._proxyInfo = Gio.DBusInterfaceInfo.new_for_xml(EOS_PAYG_IFACE);
+
+ this._codeExpiredId = 0;
+ this._propertiesChangedId = 0;
+ this._expirationReminderId = 0;
+
+ this._proxy = new Gio.DBusProxy({
+ g_connection: Gio.DBus.system,
+ g_interface_name: this._proxyInfo.name,
+ g_interface_info: this._proxyInfo,
+ g_name: EOS_PAYG_NAME,
+ g_object_path: EOS_PAYG_PATH,
+ g_flags: Gio.DBusProxyFlags.NONE,
+ });
+
+ this._proxy.init_async(GLib.PRIORITY_DEFAULT, null, this._onProxyConstructed.bind(this));
+
+ for (let errorCode in DBusErrorsMapping)
+ Gio.DBusError.register_error(PaygErrorDomain, PaygError[errorCode],
DBusErrorsMapping[errorCode]);
+ }
+
+ _onProxyConstructed(object, res) {
+ let success = false;
+ try {
+ success = object.init_finish(res);
+ } catch (e) {
+ logError(e, 'Error while constructing D-Bus proxy for %s'.format(EOS_PAYG_NAME));
+ }
+
+ if (success) {
+ // Don't use the setters here to prevent emitting a -changed signal
+ // on startup, which is useless and confuses the screenshield when
+ // selecting the session mode to construct the right unlock dialog.
+ this._enabled = this._proxy.Enabled;
+ this._expiryTime = this._proxy.ExpiryTime;
+ this._rateLimitEndTime = this._proxy.RateLimitEndTime;
+ this._setCodeFormat(this._proxy.CodeFormat || '^[0-9]{8}$');
+
+ this._propertiesChangedId = this._proxy.connect('g-properties-changed',
this._onPropertiesChanged.bind(this));
+ this._codeExpiredId = this._proxy.connectSignal('Expired', this._onCodeExpired.bind(this));
+
+ this._maybeNotifyUser();
+ this._updateExpirationReminders();
+ }
+
+ this._initialized = true;
+ this.emit('initialized');
+ }
+
+ _onPropertiesChanged(proxy, changedProps) {
+ let propsDict = changedProps.deep_unpack();
+ if (propsDict.hasOwnProperty('Enabled'))
+ this._setEnabled(this._proxy.Enabled);
+
+ if (propsDict.hasOwnProperty('ExpiryTime'))
+ this._setExpiryTime(this._proxy.ExpiryTime);
+
+ if (propsDict.hasOwnProperty('RateLimitEndTime'))
+ this._setRateLimitEndTime(this._proxy.RateLimitEndTime);
+
+ if (propsDict.hasOwnProperty('CodeFormat'))
+ this._setCodeFormat(this._proxy.CodeFormat, true);
+ }
+
+ _setEnabled(value) {
+ if (this._enabled === value)
+ return;
+
+ this._enabled = value;
+ this.emit('enabled-changed', this._enabled);
+ }
+
+ _setExpiryTime(value) {
+ if (this._expiryTime === value)
+ return;
+
+ this._expiryTime = value;
+ this._updateExpirationReminders();
+
+ this.emit('expiry-time-changed', this._expiryTime);
+ }
+
+ _setRateLimitEndTime(value) {
+ if (this._rateLimitEndTime === value)
+ return;
+
+ this._rateLimitEndTime = value;
+ this.emit('rate-limit-end-time-changed', this._rateLimitEndTime);
+ }
+
+ _setCodeFormat(value, notify = false) {
+ if (this._codeFormat === value)
+ return;
+
+ this._codeFormat = value;
+ try {
+ this._codeFormatRegex = new GLib.Regex(
+ this._codeFormat,
+ GLib.RegexCompileFlags.DOLLAR_ENDONLY,
+ GLib.RegexMatchFlags.PARTIAL);
+ } catch (e) {
+ logError(e, 'Error compiling CodeFormat regex: %s'.format(this._codeFormat));
+ this._codeFormatRegex = null;
+ }
+
+ if (notify)
+ this.emit('code-format-changed');
+ }
+
+ _onCodeExpired() {
+ this.emit('code-expired');
+ }
+
+ _clockUpdated() {
+ this._updateExpirationReminders();
+ }
+
+ _notifyPaygReminder(secondsLeft) {
+ // Only notify when in an regular session, not in GDM or initial-setup.
+ if (Main.sessionMode.currentMode != 'user' &&
+ Main.sessionMode.currentMode != 'endless' &&
+ Main.sessionMode.currentMode != 'user-coding')
+ return;
+
+ if (this._notification)
+ this._notification.destroy();
+
+ let source = new MessageTray.SystemNotificationSource();
+ Main.messageTray.add(source);
+
+ let timeLeft = timeToString(secondsLeft);
+ this._notification = new MessageTray.Notification(
+ source,
+ NOTIFICATION_TITLE_TEXT,
+ NOTIFICATION_DETAILED_FORMAT_STRING.format(timeLeft));
+ this._notification.setUrgency(MessageTray.Urgency.HIGH);
+ this._notification.setTransient(false);
+ source.notify(this._notification);
+
+ this._notification.connect('destroy', () => this._notification = null);
+ }
+
+ _maybeNotifyUser() {
+ // Sanity check.
+ if (notificationAlertTimesSecs.length === 0)
+ return;
+
+ let secondsLeft = this.timeRemainingSecs();
+ if (secondsLeft > 0 && secondsLeft <= notificationAlertTimesSecs[0])
+ this._notifyPaygReminder(secondsLeft);
+ }
+
+ _updateExpirationReminders() {
+ if (this._expirationReminderId > 0) {
+ GLib.source_remove(this._expirationReminderId);
+ this._expirationReminderId = 0;
+ }
+
+ let secondsLeft = this.timeRemainingSecs();
+
+ // The interval passed to timeout_add_seconds needs to be a 32-bit
+ // unsigned integer, so don't bother with notifications otherwise.
+ if (secondsLeft <= 0 || secondsLeft >= GLib.MAXUINT32)
+ return;
+
+ // Look for the right time to set the alarm for.
+ let targetAlertTime = 0;
+ for (let alertTime of notificationAlertTimesSecs) {
+ if (secondsLeft > alertTime) {
+ targetAlertTime = alertTime;
+ break;
+ }
+ }
+
+ // Too late to set up an alarm now.
+ if (targetAlertTime == 0)
+ return;
+
+ this._expirationReminderId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ secondsLeft - targetAlertTime,
+ () => {
+ // We want to show "round" numbers in the notification, matching
+ // whatever is specified in the notificationAlertTimeSecs array.
+ this._notifyPaygReminder(targetAlertTime);
+
+ // Reset _expirationReminderId before _updateExpirationReminders()
+ // to prevent an attempt to remove the same GSourceFunc twice.
+ this._expirationReminderId = 0;
+ this._updateExpirationReminders();
+
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ timeRemainingSecs() {
+ if (!this._enabled)
+ return Number.MAX_SAFE_INTEGER;
+
+ return Math.max(0, this._expiryTime - (Shell.util_get_boottime() / GLib.USEC_PER_SEC));
+ }
+
+ addCode(code, callback) {
+ if (!this._proxy) {
+ log('Unable to add PAYG code: No D-Bus proxy for %s'.format(EOS_PAYG_NAME));
+ return;
+ }
+
+ this._proxy.AddCodeRemote(code, (result, error) => {
+ if (callback)
+ callback(error);
+ });
+ }
+
+ clearCode() {
+ if (!this._proxy) {
+ log('Unable to clear PAYG code: No D-Bus proxy for %s'.format(EOS_PAYG_NAME));
+ return;
+ }
+
+ this._proxy.ClearCodeRemote();
+ }
+
+ validateCode(code, partial = false) {
+ if (!this._codeFormatRegex) {
+ log('Unable to validate PAYG code: no regex');
+ return false;
+ }
+
+ let [isMatch, matchInfo] = this._codeFormatRegex.match(code, 0);
+ return isMatch || (partial && matchInfo.is_partial_match());
+ }
+
+ get initialized() {
+ return this._initialized;
+ }
+
+ get enabled() {
+ return this._enabled;
+ }
+
+ get expiryTime() {
+ return this._expiryTime;
+ }
+
+ get rateLimitEndTime() {
+ return this._rateLimitEndTime;
+ }
+
+ get isLocked() {
+ if (!this.enabled)
+ return false;
+
+ return this.timeRemainingSecs() <= 0;
+ }
+});
diff --git a/js/ui/main.js b/js/ui/main.js
index 4a2e2e27bb..1d52420779 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -30,6 +30,7 @@ const PadOsd = imports.ui.padOsd;
const Panel = imports.ui.panel;
const Params = imports.misc.params;
const ParentalControlsManager = imports.misc.parentalControlsManager;
+const PaygManager = imports.misc.paygManager;
const RunDialog = imports.ui.runDialog;
const Layout = imports.ui.layout;
const LoginManager = imports.misc.loginManager;
@@ -46,6 +47,7 @@ const WindowManager = imports.ui.windowManager;
const Magnifier = imports.ui.magnifier;
const XdndHandler = imports.ui.xdndHandler;
const KbdA11yDialog = imports.ui.kbdA11yDialog;
+const Util = imports.misc.util;
const LocatePointer = imports.ui.locatePointer;
const PointerA11yTimeout = imports.ui.pointerA11yTimeout;
const WorkspaceMonitor = imports.ui.workspaceMonitor;
@@ -91,6 +93,7 @@ var inputMethod = null;
var introspectService = null;
var locatePointer = null;
var discoveryFeed = null;
+var paygManager = null;
var workspaceMonitor = null;
let _startDate;
let _defaultCssStylesheet = null;
@@ -201,6 +204,10 @@ function _initializeUI() {
magnifier = new Magnifier.Magnifier();
locatePointer = new LocatePointer.LocatePointer();
+ // The ScreenShield depends on the PaygManager, so this
+ // module needs to be initialized first.
+ paygManager = new PaygManager.PaygManager();
+
if (LoginManager.canLock())
screenShield = new ScreenShield.ScreenShield();
diff --git a/po/POTFILES.in b/po/POTFILES.in
index f077a6b7a5..715792cc44 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -13,6 +13,7 @@ js/extensionPrefs/data/ui/extensions-window.ui
js/gdm/authPrompt.js
js/gdm/loginDialog.js
js/gdm/util.js
+js/misc/paygManager.js
js/misc/systemActions.js
js/misc/util.js
js/portalHelper/main.js
diff --git a/src/shell-util.c b/src/shell-util.c
index bb2329536b..dde92d5276 100644
--- a/src/shell-util.c
+++ b/src/shell-util.c
@@ -757,3 +757,27 @@ shell_util_get_translated_folder_name (const char *name)
{
return shell_app_cache_translate_folder (shell_app_cache_get_default (), name);
}
+
+/**
+ * shell_util_get_boottime:
+ *
+ * Like g_get_monotonic_time(), but also includes any time the system is
+ * suspended. Uses `CLOCK_BOOTTIME`, hence the name, but is not guaranteed to be
+ * the time since boot.
+ *
+ * Returns: the time since some unspecified starting point, in microseconds
+ */
+gint64
+shell_util_get_boottime (void)
+{
+ struct timespec ts;
+ gint result;
+
+ result = clock_gettime (CLOCK_BOOTTIME, &ts);
+
+ if (G_UNLIKELY (result != 0))
+ g_error ("clock_gettime (CLOCK_BOOTTIME) failed: %s",
+ g_strerror (errno));
+
+ return (((gint64) ts.tv_sec) * 1000000) + (ts.tv_nsec / 1000);
+}
diff --git a/src/shell-util.h b/src/shell-util.h
index aa79f49736..69d95aae1b 100644
--- a/src/shell-util.h
+++ b/src/shell-util.h
@@ -80,6 +80,8 @@ char *shell_util_get_translated_folder_name (const char *name);
gint shell_util_get_uid (void);
+gint64 shell_util_get_boottime (void);
+
G_END_DECLS
#endif /* __SHELL_UTIL_H__ */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]