timeLimitsManager: Fully handle daily-limit-enabled setting

Separate out how it’s handled from the `history-enabled` setting,
allowing screen time usage data to be recorded with or without limits
being enforced.

This follows on from the previous commit.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>

Helps: https://gitlab.gnome.org/GNOME/gnome-control-center/-/issues/3306
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3610>
This commit is contained in:
Philip Withnall 2025-01-24 12:07:40 +00:00 committed by Marge Bot
parent 73565e582c
commit 11d8b9337d
2 changed files with 56 additions and 9 deletions

View File

@ -40,9 +40,10 @@ const GRAYSCALE_SATURATION = 1.0; // saturation ([0.0, 1.0]) when grayscale mod
/** @enum {number} */ /** @enum {number} */
export const TimeLimitsState = { export const TimeLimitsState = {
/* screen time limits are disabled */ /* screen time limit history is disabled */
DISABLED: 0, DISABLED: 0,
/* screen time limits are enabled, but the limit has not been hit yet */ /* screen time limit history recording is enabled, but limits are disabled or
* the limit has not been hit yet */
ACTIVE: 1, ACTIVE: 1,
/* limit has been reached */ /* limit has been reached */
LIMIT_REACHED: 2, LIMIT_REACHED: 2,
@ -79,7 +80,7 @@ function userStateToString(userState) {
/** /**
* A manager class which tracks total active/inactive time for a user, and * A manager class which tracks total active/inactive time for a user, and
* signals when the user has reached their daily time limit for actively using * signals when the user has reached their daily time limit for actively using
* the device. * the device, if limits are enabled.
* *
* Active/Inactive time is based off the total time the user account has spent * 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 * logged in to at least one active session, not idle (and not locked, but
@ -107,6 +108,10 @@ export const TimeLimitsManager = GObject.registerClass({
'daily-limit-time', null, null, 'daily-limit-time', null, null,
GObject.ParamFlags.READABLE, GObject.ParamFlags.READABLE,
0, GLib.MAX_UINT64, 0), 0, GLib.MAX_UINT64, 0),
'daily-limit-enabled': GObject.ParamSpec.boolean(
'daily-limit-enabled', null, null,
GObject.ParamFlags.READABLE,
true),
'grayscale-enabled': GObject.ParamSpec.boolean( 'grayscale-enabled': GObject.ParamSpec.boolean(
'grayscale-enabled', null, null, 'grayscale-enabled', null, null,
GObject.ParamFlags.READABLE, GObject.ParamFlags.READABLE,
@ -151,6 +156,7 @@ export const TimeLimitsManager = GObject.registerClass({
this._screenTimeLimitSettings.connectObject( this._screenTimeLimitSettings.connectObject(
'changed', () => this._updateSettings(), 'changed', () => this._updateSettings(),
'changed::daily-limit-seconds', () => this.notify('daily-limit-time'), 'changed::daily-limit-seconds', () => this.notify('daily-limit-time'),
'changed::daily-limit-enabled', () => this.notify('daily-limit-enabled'),
'changed::grayscale', () => this.notify('grayscale-enabled'), 'changed::grayscale', () => this.notify('grayscale-enabled'),
this); this);
@ -612,6 +618,9 @@ export const TimeLimitsManager = GObject.registerClass({
* @returns {number} * @returns {number}
*/ */
_calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs) { _calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs) {
console.assert(this.dailyLimitEnabled,
'Daily limit reached-at time only makes sense if limits are enabled');
// NOTE: This might return -1. // NOTE: This might return -1.
const firstTransitionTodayIdx = this._stateTransitions.findIndex(e => e['wallTimeSecs'] >= startOfTodaySecs); const firstTransitionTodayIdx = this._stateTransitions.findIndex(e => e['wallTimeSecs'] >= startOfTodaySecs);
@ -656,11 +665,13 @@ export const TimeLimitsManager = GObject.registerClass({
// Work out how much time the user has spent at the screen today. // Work out how much time the user has spent at the screen today.
const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs); const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs);
const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); 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: Active time today: ' +
`${activeTimeTodaySecs}s, daily limit ${dailyLimitSecs}s`); `${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`);
if (activeTimeTodaySecs >= dailyLimitSecs) { if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
newState = TimeLimitsState.LIMIT_REACHED; newState = TimeLimitsState.LIMIT_REACHED;
// Schedule an update for when the limit will be reset again. // Schedule an update for when the limit will be reset again.
@ -669,7 +680,8 @@ export const TimeLimitsManager = GObject.registerClass({
newState = TimeLimitsState.ACTIVE; newState = TimeLimitsState.ACTIVE;
// Schedule an update for when we expect the limit will be reached. // Schedule an update for when we expect the limit will be reached.
this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs); if (dailyLimitEnabled)
this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs);
} else { } else {
// User is inactive, so no point scheduling anything until they become // User is inactive, so no point scheduling anything until they become
// active again. // active again.
@ -728,6 +740,9 @@ export const TimeLimitsManager = GObject.registerClass({
case TimeLimitsState.DISABLED: case TimeLimitsState.DISABLED:
return 0; return 0;
case TimeLimitsState.ACTIVE: { case TimeLimitsState.ACTIVE: {
if (!this.dailyLimitEnabled)
return 0;
const nowSecs = this.getCurrentTime(); const nowSecs = this.getCurrentTime();
const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs); const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs);
const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs); const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs);
@ -757,6 +772,18 @@ 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() {
return this._screenTimeLimitSettings.get_boolean('daily-limit-enabled');
}
/** /**
* Whether the screen should be made grayscale once the daily limit is * Whether the screen should be made grayscale once the daily limit is
* reached. * reached.
@ -782,13 +809,15 @@ class TimeLimitsDispatcher extends GObject.Object {
this._manager = manager; this._manager = manager;
this._manager.connectObject( this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this), 'notify::state', this._onStateChanged.bind(this),
'notify::daily-limit-enabled', this._onStateChanged.bind(this),
'notify::grayscale-enabled', this._onStateChanged.bind(this), 'notify::grayscale-enabled', this._onStateChanged.bind(this),
this); this);
this._notificationSource = null; this._notificationSource = null;
this._desaturationEffect = null; this._desaturationEffect = null;
if (this._manager.state === TimeLimitsState.DISABLED) if (this._manager.state === TimeLimitsState.DISABLED ||
!this._manager.dailyLimitEnabled)
this._ensureDisabled(); this._ensureDisabled();
else else
this._ensureEnabled(); this._ensureEnabled();
@ -829,8 +858,12 @@ class TimeLimitsDispatcher extends GObject.Object {
break; break;
case TimeLimitsState.ACTIVE: { case TimeLimitsState.ACTIVE: {
this._ensureEnabled(); if (this._manager.dailyLimitEnabled) {
this._desaturationEffect.set_enabled(false); this._ensureEnabled();
this._desaturationEffect.set_enabled(false);
} else {
this._ensureDisabled();
}
break; break;
} }
@ -880,6 +913,7 @@ class TimeLimitsNotificationSource extends GObject.Object {
this._manager = manager; this._manager = manager;
this._manager.connectObject( this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this), 'notify::state', this._onStateChanged.bind(this),
'notify::daily-limit-enabled', this._onStateChanged.bind(this),
'notify::daily-limit-time', this._onStateChanged.bind(this), 'notify::daily-limit-time', this._onStateChanged.bind(this),
this); this);
@ -961,6 +995,12 @@ class TimeLimitsNotificationSource extends GObject.Object {
break; break;
case TimeLimitsState.ACTIVE: { case TimeLimitsState.ACTIVE: {
// Remove active notifications if limits have been disabled.
if (!this._manager.dailyLimitEnabled) {
this.destroy();
break;
}
// Work out when the time limit will be, and display some warnings // Work out when the time limit will be, and display some warnings
// that its impending. // that its impending.
const limitDueTime = this._manager.dailyLimitTime; const limitDueTime = this._manager.dailyLimitTime;

View File

@ -63,6 +63,7 @@ class TestHarness {
this._settings = settings; this._settings = settings;
this._settingsChangedCallback = null; this._settingsChangedCallback = null;
this._settingsChangedDailyLimitSecondsCallback = null; this._settingsChangedDailyLimitSecondsCallback = null;
this._settingsChangedDailyLimitEnabledCallback = null;
this._settingsChangedGrayscaleCallback = null; this._settingsChangedGrayscaleCallback = null;
// These two emulate relevant bits of the o.fdo.login1.User API // These two emulate relevant bits of the o.fdo.login1.User API
@ -446,12 +447,14 @@ class TestHarness {
const [ const [
changedStr, changedCallback, changedStr, changedCallback,
changedDailyLimitSecondsStr, changedDailyLimitSecondsCallback, changedDailyLimitSecondsStr, changedDailyLimitSecondsCallback,
changedDailyLimitEnabledStr, changedDailyLimitEnabledCallback,
changedGrayscaleStr, changedGrayscaleCallback, changedGrayscaleStr, changedGrayscaleCallback,
obj, obj,
] = args; ] = args;
if (changedStr !== 'changed' || if (changedStr !== 'changed' ||
changedDailyLimitSecondsStr !== 'changed::daily-limit-seconds' || changedDailyLimitSecondsStr !== 'changed::daily-limit-seconds' ||
changedDailyLimitEnabledStr !== 'changed::daily-limit-enabled' ||
changedGrayscaleStr !== 'changed::grayscale' || changedGrayscaleStr !== 'changed::grayscale' ||
typeof obj !== 'object') typeof obj !== 'object')
fail('Gio.Settings.connectObject() not called in expected way'); fail('Gio.Settings.connectObject() not called in expected way');
@ -460,6 +463,7 @@ class TestHarness {
this._settingsChangedCallback = changedCallback; this._settingsChangedCallback = changedCallback;
this._settingsChangedDailyLimitSecondsCallback = changedDailyLimitSecondsCallback; this._settingsChangedDailyLimitSecondsCallback = changedDailyLimitSecondsCallback;
this._settingsChangedDailyLimitEnabledCallback = changedDailyLimitEnabledCallback;
this._settingsChangedGrayscaleCallback = changedGrayscaleCallback; this._settingsChangedGrayscaleCallback = changedGrayscaleCallback;
}, },
get_boolean: key => { get_boolean: key => {
@ -514,6 +518,9 @@ class TestHarness {
if (event.key === 'daily-limit-seconds' && if (event.key === 'daily-limit-seconds' &&
this._settingsChangedDailyLimitSecondsCallback) this._settingsChangedDailyLimitSecondsCallback)
this._settingsChangedDailyLimitSecondsCallback(event.key); this._settingsChangedDailyLimitSecondsCallback(event.key);
if (event.key === 'daily-limit-enabled' &&
this._settingsChangedDailyLimitEnabledCallback)
this._settingsChangedDailyLimitEnabledCallback(event.key);
if (event.key === 'grayscale' && if (event.key === 'grayscale' &&
this._settingsChangedGrayscaleCallback) this._settingsChangedGrayscaleCallback)
this._settingsChangedGrayscaleCallback(event.key); this._settingsChangedGrayscaleCallback(event.key);