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} */
export const TimeLimitsState = {
/* screen time limits are disabled */
/* screen time limit history is disabled */
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,
/* limit has been reached */
LIMIT_REACHED: 2,
@ -79,7 +80,7 @@ function userStateToString(userState) {
/**
* 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
* the device.
* the device, if limits are enabled.
*
* 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
@ -107,6 +108,10 @@ export const TimeLimitsManager = GObject.registerClass({
'daily-limit-time', null, null,
GObject.ParamFlags.READABLE,
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', null, null,
GObject.ParamFlags.READABLE,
@ -151,6 +156,7 @@ export const TimeLimitsManager = GObject.registerClass({
this._screenTimeLimitSettings.connectObject(
'changed', () => this._updateSettings(),
'changed::daily-limit-seconds', () => this.notify('daily-limit-time'),
'changed::daily-limit-enabled', () => this.notify('daily-limit-enabled'),
'changed::grayscale', () => this.notify('grayscale-enabled'),
this);
@ -612,6 +618,9 @@ export const TimeLimitsManager = GObject.registerClass({
* @returns {number}
*/
_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.
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.
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: ' +
`${activeTimeTodaySecs}s, daily limit ${dailyLimitSecs}s`);
`${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`);
if (activeTimeTodaySecs >= dailyLimitSecs) {
if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
newState = TimeLimitsState.LIMIT_REACHED;
// Schedule an update for when the limit will be reset again.
@ -669,6 +680,7 @@ export const TimeLimitsManager = GObject.registerClass({
newState = TimeLimitsState.ACTIVE;
// Schedule an update for when we expect the limit will be reached.
if (dailyLimitEnabled)
this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs);
} else {
// User is inactive, so no point scheduling anything until they become
@ -728,6 +740,9 @@ export const TimeLimitsManager = GObject.registerClass({
case TimeLimitsState.DISABLED:
return 0;
case TimeLimitsState.ACTIVE: {
if (!this.dailyLimitEnabled)
return 0;
const nowSecs = this.getCurrentTime();
const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs);
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
* reached.
@ -782,13 +809,15 @@ class TimeLimitsDispatcher extends GObject.Object {
this._manager = manager;
this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this),
'notify::daily-limit-enabled', this._onStateChanged.bind(this),
'notify::grayscale-enabled', this._onStateChanged.bind(this),
this);
this._notificationSource = null;
this._desaturationEffect = null;
if (this._manager.state === TimeLimitsState.DISABLED)
if (this._manager.state === TimeLimitsState.DISABLED ||
!this._manager.dailyLimitEnabled)
this._ensureDisabled();
else
this._ensureEnabled();
@ -829,8 +858,12 @@ class TimeLimitsDispatcher extends GObject.Object {
break;
case TimeLimitsState.ACTIVE: {
if (this._manager.dailyLimitEnabled) {
this._ensureEnabled();
this._desaturationEffect.set_enabled(false);
} else {
this._ensureDisabled();
}
break;
}
@ -880,6 +913,7 @@ class TimeLimitsNotificationSource extends GObject.Object {
this._manager = manager;
this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this),
'notify::daily-limit-enabled', this._onStateChanged.bind(this),
'notify::daily-limit-time', this._onStateChanged.bind(this),
this);
@ -961,6 +995,12 @@ class TimeLimitsNotificationSource extends GObject.Object {
break;
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
// that its impending.
const limitDueTime = this._manager.dailyLimitTime;

View File

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