diff --git a/data/dbus-interfaces/org.freedesktop.MalcontentTimer1.Child.xml b/data/dbus-interfaces/org.freedesktop.MalcontentTimer1.Child.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ee5e714ba617f86e27d2522a5c85c340a79b99b4
--- /dev/null
+++ b/data/dbus-interfaces/org.freedesktop.MalcontentTimer1.Child.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index 714e65a0c9c858670595c9c2db77f1668482bace..01c11f2e652151a8b72b73465b48422b12b4e5f4 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -15,6 +15,7 @@
org.freedesktop.login1.Manager.xml
org.freedesktop.login1.Session.xml
org.freedesktop.login1.User.xml
+ org.freedesktop.MalcontentTimer1.Child.xml
org.freedesktop.ModemManager1.Modem.Modem3gpp.xml
org.freedesktop.ModemManager1.Modem.ModemCdma.xml
org.freedesktop.ModemManager1.Modem.xml
diff --git a/js/misc/parentalControlsManager.js b/js/misc/parentalControlsManager.js
index 3dfe13d5a8f933482ed7b9cb0322517140ad0446..f4b284120bb5bd7048022a4e13e242945433d1ee 100644
--- a/js/misc/parentalControlsManager.js
+++ b/js/misc/parentalControlsManager.js
@@ -32,8 +32,13 @@ let Malcontent = null;
if (HAVE_MALCONTENT) {
({default: Malcontent} = await import('gi://Malcontent?version=0'));
Gio._promisify(Malcontent.Manager.prototype, 'get_app_filter_async');
+ Gio._promisify(Malcontent.Manager.prototype, 'get_session_limits_async');
}
+// We require libmalcontent ≥ 0.14.0 for session limits
+const HAVE_MALCONTENT_0_14 = Malcontent &&
+ GObject.signal_lookup('session-limits-changed', Malcontent.Manager) > 0;
+
let _singleton = null;
/**
@@ -53,6 +58,7 @@ export function getDefault() {
const ParentalControlsManager = GObject.registerClass({
Signals: {
'app-filter-changed': {},
+ 'session-limits-changed': {},
},
}, class ParentalControlsManager extends GObject.Object {
_init() {
@@ -61,6 +67,7 @@ const ParentalControlsManager = GObject.registerClass({
this._initialized = false;
this._disabled = false;
this._appFilter = null;
+ this._sessionLimits = null;
this._initializeManager();
}
@@ -70,6 +77,7 @@ const ParentalControlsManager = GObject.registerClass({
console.debug('Skipping parental controls support, malcontent not found');
this._initialized = true;
this.emit('app-filter-changed');
+ this.emit('session-limits-changed');
return;
}
@@ -77,16 +85,22 @@ const ParentalControlsManager = GObject.registerClass({
const connection = await Gio.DBus.get(Gio.BusType.SYSTEM, null);
this._manager = new Malcontent.Manager({connection});
this._appFilter = await this._getAppFilter();
+
+ if (HAVE_MALCONTENT_0_14)
+ this._sessionLimits = await this._getSessionLimits();
} catch (e) {
logError(e, 'Failed to get parental controls settings');
return;
}
this._manager.connect('app-filter-changed', this._onAppFilterChanged.bind(this));
+ if (HAVE_MALCONTENT_0_14)
+ this._manager.connect('session-limits-changed', this._onSessionLimitsChanged.bind(this));
// Signal initialisation is complete.
this._initialized = true;
this.emit('app-filter-changed');
+ this.emit('session-limits-changed');
}
async _getAppFilter() {
@@ -123,6 +137,40 @@ const ParentalControlsManager = GObject.registerClass({
}
}
+ async _getSessionLimits() {
+ let sessionLimits = null;
+
+ try {
+ sessionLimits = await this._manager.get_session_limits_async(
+ Shell.util_get_uid(),
+ Malcontent.ManagerGetValueFlags.NONE,
+ null);
+ } catch (e) {
+ if (!e.matches(Malcontent.ManagerError, Malcontent.ManagerError.DISABLED))
+ throw e;
+
+ console.debug('Parental controls globally disabled');
+ this._disabled = true;
+ }
+
+ return sessionLimits;
+ }
+
+ async _onSessionLimitsChanged(manager, uid) {
+ // Emit 'changed' signal only if session-limits are changed for currently logged-in user
+ const currentUid = Shell.util_get_uid();
+ if (currentUid !== uid)
+ return;
+
+ try {
+ this._sessionLimits = await this._getSessionLimits();
+ this.emit('session-limits-changed');
+ } catch (e) {
+ // Keep the old session limits
+ logError(e, `Failed to get new MctSessionLimits for uid ${Shell.util_get_uid()} on session-limits-changed`);
+ }
+ }
+
get initialized() {
return this._initialized;
}
@@ -152,4 +200,24 @@ const ParentalControlsManager = GObject.registerClass({
return this._appFilter.is_appinfo_allowed(appInfo);
}
+
+ /**
+ * Check whether parental controls session limits are enabled for the account,
+ * and if the library supports the features required for the functionality.
+ *
+ * @returns {bool} whether session limits are supported and enabled.
+ */
+ sessionLimitsEnabled() {
+ // Are latest parental controls enabled (at configure or runtime)?
+ if (!HAVE_MALCONTENT_0_14 || this._disabled)
+ return false;
+
+ // Have we finished initialising yet?
+ if (!this.initialized) {
+ console.debug('Ignoring session limits because parental controls not yet initialised');
+ return false;
+ }
+
+ return this._sessionLimits.is_enabled();
+ }
});
diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js
index 79642b9debbe3f4432ce5cf881f00b88c198c068..c0dd97ce9d66732cb076077353c54befc48f01f7 100644
--- a/js/misc/timeLimitsManager.js
+++ b/js/misc/timeLimitsManager.js
@@ -32,6 +32,13 @@ import * as Gettext from 'gettext';
import * as LoginManager from './loginManager.js';
import * as Main from '../ui/main.js';
import * as MessageTray from '../ui/messageTray.js';
+import * as ParentalControlsManager from './parentalControlsManager.js';
+import * as SystemActions from './systemActions.js';
+
+import {loadInterfaceXML} from './fileUtils.js';
+
+const TimerChildIface = loadInterfaceXML('org.freedesktop.MalcontentTimer1.Child');
+const TimerChildProxy = Gio.DBusProxy.makeProxyWrapper(TimerChildIface);
export const HISTORY_THRESHOLD_SECONDS = 14 * 7 * 24 * 60 * 60; // maximum time history entries are kept
const LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS = 10 * 60; // notify the user 10min before their limit is reached
@@ -79,10 +86,17 @@ function userStateToString(userState) {
/**
* A manager class which tracks total active/inactive time for a user, and
+ * signals when the user has reached their session time limit for actively
+ * using the device if the user has parental controls session limits set. When
+ * they are disabled, the wellbeing daily time limit is considered, and manager
* signals when the user has reached their daily time limit for actively using
* the device, if limits are enabled.
*
- * Active/Inactive time is based off the total time the user account has spent
+ * For parental controls session time limit, the manager tracks the active/inactive
+ * time via malcontent-timerd.
+ *
+ * For wellbeing daily time limit, the way in which the manager tracks the
+ * active/inactive time is based off the total time the user account has spent
* logged in to at least one active session, not idle (and not locked, but
* that’s a subset of idle time), and not suspended.
* This corresponds to the `active` state from sd_uid_get_state()
@@ -123,7 +137,7 @@ export const TimeLimitsManager = GObject.registerClass({
'daily-limit-reached': {},
},
}, class TimeLimitsManager extends GObject.Object {
- constructor(historyFile, clock, loginManagerFactory, loginUserFactory, settingsFactory) {
+ constructor(historyFile, clock, loginManagerFactory, loginUserFactory, settingsFactory, parentalControlsManagerFactory, timerChildProxyFactory) {
super();
// Allow these few bits of global state to be overridden for unit testing
@@ -154,12 +168,43 @@ export const TimeLimitsManager = GObject.registerClass({
return loginManager.getCurrentUserProxy();
},
};
+
+ this._parentalControlsManagerFactory = parentalControlsManagerFactory ?? {
+ new: ParentalControlsManager.getDefault,
+ };
+ this._parentalControlsManager = this._parentalControlsManagerFactory.new();
+
+ this._parentalControlsManager.connectObject(
+ 'session-limits-changed', () => this._onSessionLimitsChanged().catch(logError),
+ this);
+
+ this._estimatedTimes = [];
+ this._timerChildProxyFactory = timerChildProxyFactory ?? {
+ new: () => {
+ return TimerChildProxy(Gio.DBus.system,
+ 'org.freedesktop.MalcontentTimer1',
+ '/org/freedesktop/MalcontentTimer1',
+ (proxy, error) => {
+ if (error)
+ console.debug(`Failed to get TimerChild proxy: ${error}`);
+ },
+ null, /* cancellable */
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION
+ );
+ },
+ };
+
+ this._timerChildProxy = this._timerChildProxyFactory.new();
+
+ this._timerChildProxy.connectSignal('EstimatedTimesChanged',
+ () => this._updateEstimatedTimes().catch(logError));
+
this._settingsFactory = settingsFactory ?? {
new: Gio.Settings.new,
};
this._screenTimeLimitSettings = this._settingsFactory.new('org.gnome.desktop.screen-time-limits');
this._screenTimeLimitSettings.connectObject(
- 'changed', () => this._updateSettings(),
+ 'changed', () => this._updateStateMachine(),
'changed::daily-limit-seconds', () => this.notify('daily-limit-time'),
'changed::daily-limit-enabled', () => this.notify('daily-limit-enabled'),
'changed::grayscale', () => this.notify('grayscale-enabled'),
@@ -176,13 +221,68 @@ export const TimeLimitsManager = GObject.registerClass({
this._timeChangeId = 0;
this._clockOffsetSecs = 0;
this._ignoreClockOffsetChanges = false;
+ this._latestUsageEndSecs = 0;
// Start tracking timings
- this._updateSettings();
+ this._updateStateMachine();
+ }
+
+ async _recordUsage(startSecs, endSecs) {
+ console.debug('Recording usage to the parental controls daemon: ' +
+ `${this._unixToString(startSecs)} ${this._unixToString(endSecs)}`);
+ try {
+ await this._timerChildProxy.RecordUsageAsync(
+ [[startSecs, endSecs, 'login-session', '']]);
+ this._latestUsageEndSecs = endSecs;
+ } catch (e) {
+ if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) {
+ // Limits should only be enabled when the daemon is available.
+ if (this._parentalControlsManager.sessionLimitsEnabled())
+ throw e;
+ else
+ console.debug('Parental controls timer daemon not available');
+ } else {
+ console.warn(`Failed to record usage: ${e.message}`);
+ }
+ }
}
- _updateSettings() {
- if (!this._screenTimeLimitSettings.get_boolean('history-enabled')) {
+ async _onSessionLimitsChanged() {
+ // New session limits may mean that the state machine will need to start/stop.
+ this._updateStateMachine();
+
+ // Checkpoint current active usage to the parental controls timer daemon,
+ // so that it can make a correct estimate of the remaining screen time.
+ // Wellbeing monitoring uses the stored edge events (state transitions),
+ // whereas the parental controls timer daemon stores time periods only after
+ // one is finished, which is why it cannot know the start of the usage period,
+ // without recording it first. Recording will trigger EstimatedTimesChanged
+ // signal, which handler updates the estimated times and state.
+ if (this._stateTransitions.length > 0 &&
+ this._stateTransitions.at(-1).newState === UserState.ACTIVE) {
+ const startSecs = this._stateTransitions.at(-1).wallTimeSecs;
+ const endSecs = this.getCurrentTime();
+ await this._recordUsage(startSecs, endSecs);
+ }
+ }
+
+ async _updateEstimatedTimes() {
+ const [, timesSecs] = await this._timerChildProxy.GetEstimatedTimesAsync('login-session');
+ if (timesSecs[''])
+ this._estimatedTimes = timesSecs[''];
+ else
+ this._estimatedTimes = [];
+
+ this._updateState();
+ }
+
+ _storingTransitionsEnabled() {
+ return this._parentalControlsManager.sessionLimitsEnabled() ||
+ this._screenTimeLimitSettings.get_boolean('history-enabled');
+ }
+
+ _updateStateMachine() {
+ if (!this._storingTransitionsEnabled()) {
if (this._state !== TimeLimitsState.DISABLED) {
this._stopStateMachine().catch(
e => console.warn(`Failed to stop state machine: ${e.message}`));
@@ -190,7 +290,7 @@ export const TimeLimitsManager = GObject.registerClass({
return false;
}
- // If this is the first time _updateSettings() has been called, start
+ // If this is the first time _updateStateMachine() has been called, start
// the state machine.
if (this._state === TimeLimitsState.DISABLED) {
this._startStateMachine().catch(
@@ -308,7 +408,7 @@ export const TimeLimitsManager = GObject.registerClass({
this._lastStateChangeTimeSecs = 0;
this.notify('state');
- if (this._screenTimeLimitSettings.get_boolean('history-enabled')) {
+ if (this._storingTransitionsEnabled()) {
// Add a fake transition to show the shutdown.
if (this._userState !== UserState.INACTIVE) {
const nowSecs = this.getCurrentTime();
@@ -467,7 +567,7 @@ export const TimeLimitsManager = GObject.registerClass({
if (debugLog) {
console.debug('TimeLimitsManager: User state changed from ' +
- `${userStateToString(oldState)} to ${userStateToString(newState)} at ${wallTimeSecs}s`);
+ `${userStateToString(oldState)} to ${userStateToString(newState)} at ${this._unixToString(wallTimeSecs)}`);
}
// This potentially changed the limit time and timeout calculations.
@@ -497,6 +597,13 @@ export const TimeLimitsManager = GObject.registerClass({
* if the system real time clock changes (relative to the monotonic clock).
*/
async _loadTransitions() {
+ // When parental controls session limits are enabled, additionally update
+ // the cached estimated times from the parental controls timer daemon.
+ if (this._parentalControlsManager.sessionLimitsEnabled()) {
+ await this._updateEstimatedTimes();
+ console.debug('TimeLimitsManager: Loaded time estimates from the timer daemon');
+ }
+
const file = this._historyFile;
let contents;
@@ -605,6 +712,30 @@ export const TimeLimitsManager = GObject.registerClass({
this._stateTransitions = newTransitions;
+ // Record all of the usage records to the parental controls timer daemon
+ // if the parental controls session limits are in place.
+ if (this._parentalControlsManager.sessionLimitsEnabled()) {
+ for (var j = 0; j < this._stateTransitions.length - 1; j++) {
+ const start = this._stateTransitions[j];
+ const end = this._stateTransitions[j + 1];
+
+ // Make sure the transition is correct
+ if (start['newState'] !== UserState.ACTIVE ||
+ end['newState'] !== UserState.INACTIVE)
+ continue;
+
+ const startSecs = start['wallTimeSecs'];
+ const endSecs = end['wallTimeSecs'];
+
+ // Avoid re-sending the old state transitions
+ if (endSecs <= this._latestUsageEndSecs)
+ continue;
+
+ /* eslint-disable-next-line no-await-in-loop */
+ await this._recordUsage(startSecs, endSecs);
+ }
+ }
+
if (this._stateTransitions.length === 0) {
try {
await file.delete(this._cancellable);
@@ -751,6 +882,17 @@ export const TimeLimitsManager = GObject.registerClass({
return 0;
}
+ /**
+ * Convert unix epoch to ISO 8601 formatted string for logging purposes.
+ *
+ * @param {number} secs Unix time to represent in human readable form
+ * @returns {string}
+ */
+ _unixToString(secs) {
+ const dateTime = GLib.DateTime.new_from_unix_local(secs);
+ return dateTime.format_iso8601();
+ }
+
_updateState() {
console.assert(this._state !== TimeLimitsState.DISABLED,
'Time limits should not be disabled when updating timer');
@@ -764,24 +906,63 @@ export const TimeLimitsManager = GObject.registerClass({
if (startOfTodaySecs > this._lastStateChangeTimeSecs)
newState = TimeLimitsState.ACTIVE;
+ const [, currentSessionStart, currentSessionEnd, nextSessionStart] = this._estimatedTimes;
+ const sessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled();
+
+ if (sessionLimitsEnabled) {
+ // Parental controls session limits have either just been enabled
+ // and the estimated times have not been cached yet, or disabled
+ // but the manager has not detected that yet, so skip updating state.
+ if (nextSessionStart === undefined || currentSessionEnd === undefined)
+ return;
+ }
+
+ const sessionLimitsDebug = sessionLimitsEnabled ? 'current session start: ' +
+ `${this._unixToString(currentSessionStart)}, current session end: ` +
+ `${this._unixToString(currentSessionEnd)}` : 'disabled';
+ console.debug('TimeLimitsManager: Parental controls session limits: ' +
+ `${sessionLimitsDebug}`);
+
// Work out how much time the user has spent at the screen today.
const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs);
const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds');
const dailyLimitEnabled = this._screenTimeLimitSettings.get_boolean('daily-limit-enabled');
const dailyLimitDebug = dailyLimitEnabled ? `${dailyLimitSecs}s` : 'disabled';
- console.debug('TimeLimitsManager: Active time today: ' +
+ console.debug('TimeLimitsManager: Wellbeing active time today: ' +
`${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`);
- if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
+
+ // Update TimeLimitsState. When the user is inactive, there's no point
+ // scheduling anything until they become active again.
+ if (sessionLimitsEnabled) {
+ if (nextSessionStart <= nowSecs) {
+ // Just entered daily schedule, so update cached estimated times
+ // which will perform state update afterwards.
+ this._updateEstimatedTimes();
+ return;
+ }
+
+ if (nowSecs >= currentSessionEnd) {
+ newState = TimeLimitsState.LIMIT_REACHED;
+
+ // Schedule an update for when the session limit will be reset again.
+ this._scheduleUpdateState(nextSessionStart - nowSecs);
+ } else if (this._userState === UserState.ACTIVE) {
+ newState = TimeLimitsState.ACTIVE;
+
+ // Schedule an update for when we expect the session limit will be reached.
+ this._scheduleUpdateState(currentSessionEnd - nowSecs);
+ }
+ } else if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
newState = TimeLimitsState.LIMIT_REACHED;
- // Schedule an update for when the limit will be reset again.
+ // Schedule an update for when the daily limit will be reset again.
this._scheduleUpdateState(startOfTomorrowSecs - nowSecs);
} else if (this._userState === UserState.ACTIVE) {
newState = TimeLimitsState.ACTIVE;
- // Schedule an update for when we expect the limit will be reached.
+ // Schedule an update for when we expect the daily limit will be reached.
if (dailyLimitEnabled)
this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs);
} else {
@@ -791,6 +972,9 @@ export const TimeLimitsManager = GObject.registerClass({
// Update the saved state.
if (newState !== this._state) {
+ console.debug('TimeLimitsManager: State changed from ' +
+ `${timeLimitsStateToString(this._state)} ` +
+ `to ${timeLimitsStateToString(newState)}`);
this._state = newState;
this._lastStateChangeTimeSecs = nowSecs;
this.notify('state');
@@ -827,6 +1011,15 @@ export const TimeLimitsManager = GObject.registerClass({
return this._state;
}
+ /**
+ * Whether the parental controls session limits are enabled.
+ *
+ * @type {boolean}
+ */
+ get sessionLimitsEnabled() {
+ return this._parentalControlsManager.sessionLimitsEnabled();
+ }
+
/**
* The time when the daily limit will be reached. If the user is currently
* active, and has not reached the limit, this is a non-zero value in the
@@ -877,9 +1070,6 @@ export const TimeLimitsManager = GObject.registerClass({
/**
* Whether the daily limit is enabled.
*
- * If false, screen usage information is recorded, but no limit is enforced.
- * reached.
- *
* @type {boolean}
*/
get dailyLimitEnabled() {
@@ -901,13 +1091,15 @@ export const TimeLimitsManager = GObject.registerClass({
* Glue class which takes the state-based output from TimeLimitsManager and
* converts it to event-based notifications for the user to tell them
* when their time limit has been reached. It factors the user’s UI preferences
- * into account.
+ * into account, and whether parental controls session limits are enabled.
*/
export const TimeLimitsDispatcher = GObject.registerClass(
class TimeLimitsDispatcher extends GObject.Object {
constructor(manager) {
super();
+ this._systemActions = new SystemActions.getDefault();
+
this._manager = manager;
this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this),
@@ -971,6 +1163,16 @@ class TimeLimitsDispatcher extends GObject.Object {
}
case TimeLimitsState.LIMIT_REACHED: {
+ if (this._manager.sessionLimitsEnabled) {
+ // Trying to lock the screen will fail if the action is not
+ // available. This happens when admin disables lock screen,
+ // or when not running under GDM/logind. Those are corner cases
+ // which don’t need to be handled.
+ if (this._systemActions.canLockScreen)
+ this._systemActions.activateLockScreen();
+ break;
+ }
+
this._ensureEnabled();
if (this._manager.grayscaleEnabled) {
@@ -1139,8 +1341,10 @@ class TimeLimitsNotificationSource extends GObject.Object {
case TimeLimitsState.LIMIT_REACHED: {
// Notify the user that they’ve reached their limit, when we
- // transition from any state to LIMIT_REACHED.
- if (this._previousState !== TimeLimitsState.LIMIT_REACHED) {
+ // transition from any state to LIMIT_REACHED. Noop if parental
+ // controls session limits are enabled, since we’re locking then.
+ if (!this._manager.sessionLimitsEnabled &&
+ this._previousState !== TimeLimitsState.LIMIT_REACHED) {
this._ensureNotification({
title: _('Screen Time Limit Reached'),
body: _('It’s time to stop using the device'),
diff --git a/tests/unit/timeLimitsManager.js b/tests/unit/timeLimitsManager.js
index bcbd77e885011dfbcdbefac6411ac1591d9fb542..d7c5a1724abb12893be23eda26eef4c38dccf15d 100644
--- a/tests/unit/timeLimitsManager.js
+++ b/tests/unit/timeLimitsManager.js
@@ -36,7 +36,9 @@ const {TimeLimitsState, UserState} = TimeLimitsManager;
* of time, maintaining an internal ordered queue of events, and providing three
* groups of mock functions which the `TimeLimitsManager` uses to interact with
* it: mock versions of GLib’s clock and timeout functions, a mock proxy of the
- * logind `User` D-Bus object, and a mock version of `Gio.Settings`.
+ * logind `User` D-Bus object, a mock proxy around `Malcontent.Manager` object,
+ * a mock proxy of the malcontent-timerd `Child` D-Bus object, and a mock
+ * version of `Gio.Settings`.
*
* The internal ordered queue of events is sorted by time (in real/wall clock
* seconds, i.e. UNIX timestamps). On each _tick(), the next event is shifted
@@ -53,7 +55,7 @@ const {TimeLimitsState, UserState} = TimeLimitsManager;
* daily time limit is reset at a specific time each morning.
*/
class TestHarness {
- constructor(settings, historyFileContents = null) {
+ constructor(settings, historyFileContents = null, sessionLimitsEnabled = false) {
this._currentTimeSecs = 0;
this._clockOffset = 100; // make the monotonic clock lag by 100s, arbitrarily
this._nextSourceId = 1;
@@ -65,6 +67,8 @@ class TestHarness {
this._settingsChangedDailyLimitSecondsCallback = null;
this._settingsChangedDailyLimitEnabledCallback = null;
this._settingsChangedGrayscaleCallback = null;
+ this._parentalControlsManagerSessionLimitsChangedCallback = null;
+ this._timerChildProxyEstimatedTimesChangedCallback = null;
// These two emulate relevant bits of the o.fdo.login1.User API
// See https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#User%20Objects
@@ -88,6 +92,13 @@ class TestHarness {
this._historyFile = file;
+ this._sessionLimitsEnabled = sessionLimitsEnabled;
+ this._limitReached = false;
+ this._currentSessionStartTimeSecs = 0;
+ this._currentSessionEstimatedEndTimeSecs = GLib.MAX_UINT64;
+ this._nextSessionStartTimeSecs = GLib.MAX_UINT64;
+ this._nextSessionEstimatedEndTimeSecs = GLib.MAX_UINT64;
+
// And a mock D-Bus proxy for logind.
const harness = this;
@@ -316,6 +327,55 @@ class TestHarness {
});
}
+ /**
+ * Add a parental controls manager session limits changed event to the event
+ * queue. This simulates the `session-limits-changed` signal of the
+ * `Malcontent.Manager` object, notifying gnome-shell that the user’s
+ * session limits were changed, for example from malcontent-control.
+ *
+ * @param {string} timeStr Date/Time the event happens, in ISO 8601 format.
+ * @param {boolean} newSessionLimitsEnabled New session limits enabled setting.
+ */
+ addParentalControlsManagerSessionLimitsChangedEvent(timeStr, newSessionLimitsEnabled) {
+ return this._insertEvent({
+ type: 'parental-controls-manager-session-limits-changed',
+ time: TestHarness.timeStrToSecs(timeStr),
+ newSessionLimitsEnabled,
+ });
+ }
+
+ /**
+ * Add a timer child proxy estimated times changed event to the event
+ * queue. This simulates the `EstimatedTimesChanged` signal of the
+ * `org.freedesktop.MalcontentTimer1.Child` object, notifying gnome-shell
+ * that the user’s estimated session times were changed, for example as a
+ * result of gnome-shell recording usage.
+ *
+ * @param {string} timeStr Date/Time the event happens, in ISO 8601 format.
+ * @param {boolean} newLimitReached New limit reached setting.
+ * @param {number} newCurrentSessionStartTimeSecs New current session start
+ * time in seconds.
+ * @param {number} newCurrentSessionEstimatedEndTimeSecs New current session
+ * estimated end time in seconds.
+ * @param {number} newNextSessionStartTimeSecs New next session start time
+ * in seconds.
+ * @param {number} newNextSessionEstimatedEndTimeSecs New next session
+ * estimated end time in seconds.
+ */
+ addTimerChildProxyEstimatedTimesChangedEvent(timeStr, newLimitReached,
+ newCurrentSessionStartTimeSecs, newCurrentSessionEstimatedEndTimeSecs,
+ newNextSessionStartTimeSecs, newNextSessionEstimatedEndTimeSecs) {
+ return this._insertEvent({
+ type: 'timer-child-proxy-estimated-times-changed',
+ time: TestHarness.timeStrToSecs(timeStr),
+ newLimitReached,
+ newCurrentSessionStartTimeSecs,
+ newCurrentSessionEstimatedEndTimeSecs,
+ newNextSessionStartTimeSecs,
+ newNextSessionEstimatedEndTimeSecs,
+ });
+ }
+
/**
* Add a settings change event to the event queue. This simulates dconf
* notifying gnome-shell that the user has changed a setting, for example
@@ -410,6 +470,29 @@ class TestHarness {
});
}
+ /**
+ * Add a state assertion event to the event queue. This is a specialised
+ * form of `addAssertionEvent()` which asserts that the given
+ * start and end date/times of the most recent usage recording equal the
+ * expected values at date/time `timeStr`.
+ *
+ * @param {string} timeStr Date/Time to check the times at, in ISO 8601
+ * format.
+ * @param {string} startStr Start date/time in ISO 8601 format.
+ * @param {string} endStr End date/time in ISO 8601 format.
+ */
+ expectUsage(timeStr, startStr, endStr) {
+ return this.addAssertionEvent(timeStr, () => {
+ const [start, end] = this._timeStore;
+ expect(start)
+ .withContext('start')
+ .toEqual(TestHarness.timeStrToSecs(startStr));
+ expect(end)
+ .withContext('end')
+ .toEqual(TestHarness.timeStrToSecs(endStr));
+ });
+ }
+
_popEvent() {
return this._events.shift();
}
@@ -480,6 +563,21 @@ class TestHarness {
this._currentTimeSecs = TestHarness.timeStrToSecs(timeStr);
}
+ /**
+ * Create `TimeLimitManager`.
+ *
+ * @returns {TimeLimitsManager} the new timeLimitsManager.
+ */
+ createTimeLimitsManager() {
+ return new TimeLimitsManager.TimeLimitsManager(this.mockHistoryFile,
+ this.mockClock,
+ this.mockLoginManagerFactory,
+ this.mockLoginUserFactory,
+ this.mockSettingsFactory,
+ this.mockParentalControlsManagerFactory,
+ this.mockTimerChildProxyFactory);
+ }
+
/**
* Get a mock login manager factory for use in the `TimeLimitsManager` under
* test. This is an object providing a constructor for the objects returned
@@ -516,6 +614,92 @@ class TestHarness {
};
}
+ /**
+ * Get a mock parental controls manager factory for use in the
+ * `TimeLimitsManager` under test. This is an object providing constructors
+ * for `ParentalControlsManager` objects, which are proxies around the
+ * `Malcontent.Manager` objects. Each constructor returns a basic
+ * implementation of `ParentalControlsManager` which uses the daily limit
+ * and daily schedule variables passed to `TestHarness` in its constructor
+ * in the settings dictionary.
+ *
+ * This has an extra layer of indirection to match `mockSettingsFactory`.
+ */
+ get mockParentalControlsManagerFactory() {
+ return {
+ new: () => {
+ return {
+ connectObject: (...args) => {
+ const [
+ sessionLimitsChangedStr, sessionLimitsChangedCallback,
+ obj,
+ ] = args;
+
+ if (sessionLimitsChangedStr !== 'session-limits-changed' ||
+ typeof obj !== 'object')
+ fail('ParentalControlsManager.connectObject() not called in expected way');
+ if (this._parentalControlsManagerSessionLimitsChangedCallback !== null)
+ fail('ParentalControlsManager signals already connected');
+
+ this._parentalControlsManagerSessionLimitsChangedCallback = sessionLimitsChangedCallback;
+ },
+ sessionLimitsEnabled: () => {
+ return this._sessionLimitsEnabled;
+ },
+ };
+ },
+ };
+ }
+
+ /**
+ * Get a mock timer child proxy factory for use in the `TimeLimitsManager`
+ * under test. This is an object providing constructors for `TimerChildProxy`
+ * objects, which are proxies around the `org.freedesktop.MalcontentTimer1.Child`
+ * D-Bus API. Each constructor returns a basic implementation of `TimerChildProxy`.
+ *
+ * This has an extra layer of indirection to match `mockSettingsFactory`.
+ */
+ get mockTimerChildProxyFactory() {
+ return {
+ new: () => {
+ return {
+ connectSignal: (...args) => {
+ const [
+ estimatedTimesChangedStr, estimatedTimesChangedCallback,
+ ] = args;
+
+ if (estimatedTimesChangedStr !== 'EstimatedTimesChanged')
+ fail('TimerChildProxy.connectSignal() not called in expected way');
+ if (this._timerChildProxyEstimatedTimesChangedCallback !== null)
+ fail('TimerChildProxy signals already connected');
+
+ this._timerChildProxyEstimatedTimesChangedCallback = estimatedTimesChangedCallback;
+ },
+ RecordUsageAsync: args => {
+ return new Promise(resolve => {
+ const [[startSecs, endSecs]] = args;
+ this._timeStore = [startSecs, endSecs];
+ resolve();
+ });
+ },
+ GetEstimatedTimesAsync: _ => {
+ return new Promise(resolve => {
+ resolve([0, {
+ '': [
+ this._limitReached,
+ this._currentSessionStartTimeSecs,
+ this._currentSessionEstimatedEndTimeSecs,
+ this._nextSessionStartTimeSecs,
+ this._nextSessionEstimatedEndTimeSecs,
+ ],
+ }]);
+ });
+ },
+ };
+ },
+ };
+ }
+
/**
* Get a mock settings factory for use in the `TimeLimitsManager` under test.
* This is an object providing constructors for `Gio.Settings` objects. Each
@@ -608,6 +792,22 @@ class TestHarness {
if (this._loginUserPropertiesChangedCallback)
this._loginUserPropertiesChangedCallback();
break;
+ case 'parental-controls-manager-session-limits-changed':
+ this._sessionLimitsEnabled = event.newSessionLimitsEnabled;
+
+ if (this._parentalControlsManagerSessionLimitsChangedCallback)
+ this._parentalControlsManagerSessionLimitsChangedCallback();
+ break;
+ case 'timer-child-proxy-estimated-times-changed':
+ this._limitReached = event.newLimitReached;
+ this._currentSessionStartTimeSecs = event.newCurrentSessionStartTimeSecs;
+ this._currentSessionEstimatedEndTimeSecs = event.newCurrentSessionEstimatedEndTimeSecs;
+ this._nextSessionStartTimeSecs = event.newNextSessionStartTimeSecs;
+ this._nextSessionEstimatedEndTimeSecs = event.newNextSessionEstimatedEndTimeSecs;
+
+ if (this._timerChildProxyEstimatedTimesChangedCallback)
+ this._timerChildProxyEstimatedTimesChangedCallback();
+ break;
case 'settings-change':
this._settings[event.schemaId][event.key] = event.newValue;
@@ -696,7 +896,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.DISABLED);
harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.DISABLED);
@@ -711,6 +911,35 @@ describe('Time limits manager', () => {
harness.run();
});
+ it('cannot be disabled via GSettings if parental controls enabled', () => {
+ const harness = new TestHarness(
+ {
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': false,
+ 'daily-limit-enabled': false,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ },
+ null,
+ {
+ 'parental-enabled': true,
+ }
+ );
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:10Z', 'active', false);
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:20Z', 'lingering', true);
+ harness.expectProperties('2024-06-01T15:00:30Z', timeLimitsManager, {
+ 'state': TimeLimitsState.ACTIVE,
+ });
+ harness.shutdownManager('2024-06-01T15:10:00Z', timeLimitsManager);
+
+ harness.run();
+ });
+
it('can be toggled on and off via GSettings', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {
@@ -720,7 +949,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.addSettingsChangeEvent('2024-06-01T11:00:00Z',
@@ -755,7 +984,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T13:59:59Z', timeLimitsManager, {
@@ -768,6 +997,37 @@ describe('Time limits manager', () => {
harness.run();
});
+ it('tracks a single day’s usage with parental controls limit enabled', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': true,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ harness.expectState('2024-06-01T10:00:03Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T12:59:59Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T13:00:01Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.shutdownManager('2024-06-01T13:00:02Z', timeLimitsManager);
+
+ harness.run();
+ });
+
it('tracks a single day’s usage early in the morning', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {
@@ -777,7 +1037,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T00:30:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T00:30:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T01:29:59Z', timeLimitsManager, {
@@ -799,7 +1059,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T13:59:59Z', timeLimitsManager, {
@@ -832,7 +1092,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T15:00:00Z', timeLimitsManager, {
@@ -872,7 +1132,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// Run until the limit is reached.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
@@ -900,6 +1160,189 @@ describe('Time limits manager', () => {
harness.run();
});
+ it('resets usage if the parental controls session limit is changed', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': true,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ // Run before the limit is reached
+ harness.expectState('2024-06-01T10:00:03Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T12:00:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ // Change session limits part-way through the day in a way which means
+ // the child still has time left that day
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T12:30:00Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T14:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T04:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T12:30:01Z', true
+ );
+
+ // Run before the limit is reached
+ harness.expectState('2024-06-01T12:30:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T13:30:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ // Change session limits part-way through the day in a way which means
+ // the child no longer has time left that day
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T13:45:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T13:45:01Z', true
+ );
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T13:45:02Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T14:00:00Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Change session limits after the child has run out of time in a way
+ // which means the child has still run out of time
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T14:30:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T14:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T04:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T14:30:01Z', true
+ );
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T14:30:02Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Change session limits after the child has run out of time in a way
+ // which means the child has more time remaining
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T15:30:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T16:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T06:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T15:30:01Z', true
+ );
+
+ // Run until the limit is reached
+ harness.expectState('2024-06-01T15:30:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T16:00:05Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T16:00:06Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T23:59:59Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // After the limit is over, GetEstimatedTimes will return new values of
+ // the current and next sessions, so change the values accordingly
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-02T00:00:00Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T06:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-03T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-03T06:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-02T00:00:01Z', true
+ );
+
+ // Run after the limit is over
+ harness.expectState('2024-06-02T00:00:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ harness.shutdownManager('2024-06-02T01:00:00Z', timeLimitsManager);
+
+ harness.run();
+ });
+
+ it('records usage correctly to the malcontent timer daemon', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': false,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T20:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T10:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ // Session limits changed
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T11:00:00Z', true
+ );
+ harness.expectUsage('2024-06-01T11:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T11:00:00Z');
+
+ // User state changed
+ harness.addLoginUserStateChangeEvent('2024-06-01T12:00:00Z', 'lingering', true);
+ harness.expectUsage('2024-06-01T12:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T12:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T13:00:00Z', 'active', false);
+ harness.expectUsage('2024-06-01T13:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T12:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T14:00:00Z', 'offline', true);
+ harness.expectUsage('2024-06-01T14:00:01Z', '2024-06-01T13:00:00Z', '2024-06-01T14:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:00Z', 'active', false);
+ harness.expectUsage('2024-06-01T15:00:01Z', '2024-06-01T13:00:00Z', '2024-06-01T14:00:00Z');
+
+ // Disable wellbeing & parental
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T16:00:00Z', false
+ );
+ harness.expectUsage('2024-06-01T16:00:01Z', '2024-06-01T15:00:00Z', '2024-06-01T16:00:00Z');
+
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T17:00:00Z', true
+ );
+ harness.expectUsage('2024-06-01T17:00:01Z', '2024-06-01T15:00:00Z', '2024-06-01T16:00:00Z');
+
+ // Shutdown
+ harness.shutdownManager('2024-06-01T18:00:00Z', timeLimitsManager);
+ harness.expectUsage('2024-06-01T18:00:00Z', '2024-06-01T17:00:00Z', '2024-06-01T18:00:00Z');
+
+ harness.run();
+ });
+
it('tracks usage correctly from an existing history file', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {
@@ -930,7 +1373,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// The existing history file (above) lists two active periods,
// 07:30–08:00 and 08:30–09:30 that morning. So the user should have
@@ -966,7 +1409,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// The existing history file (above) lists one active period,
// 04:30–08:50 that morning. So the user should have no time left today.
@@ -1005,7 +1448,7 @@ describe('Time limits manager', () => {
},
}, invalidHistoryFileContents);
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// The existing history file (above) is invalid or a no-op and
// should be ignored.
@@ -1051,7 +1494,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// The existing history file (above) lists two active periods,
// one of which is a long time ago and the other is ‘this’ morning in
@@ -1116,7 +1559,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// The existing history file (above) lists one active period,
// 04:30–08:50 that morning IN THE YEAR 3000. This could have resulted
@@ -1151,7 +1594,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.addSettingsChangeEvent('2024-06-01T10:00:02Z',
@@ -1187,7 +1630,7 @@ describe('Time limits manager', () => {
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectState('2024-06-01T14:00:01Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
@@ -1225,7 +1668,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// Use up 2h of the daily limit.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
@@ -1262,7 +1705,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// Use up 2h of the daily limit.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
@@ -1299,7 +1742,7 @@ describe('Time limits manager', () => {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = harness.createTimeLimitsManager();
// Use up 2h of the daily limit.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);