diff --git a/js/misc/timeLimitsManager.js b/js/misc/timeLimitsManager.js index e1a22f0f82771af122dbd323b6836d2fbbed8ef4..abaf3ba24922a8a25a1ea83dbce2613599fb5ad4 100644 --- a/js/misc/timeLimitsManager.js +++ b/js/misc/timeLimitsManager.js @@ -42,6 +42,7 @@ 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 +const PARENTAL_CONTROLS_LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS = 60; // notify the child 60s before their limit is reached const GRAYSCALE_FADE_TIME_SECONDS = 3; const GRAYSCALE_SATURATION = 1.0; // saturation ([0.0, 1.0]) when grayscale mode is activated, 1.0 means full desaturation @@ -175,8 +176,11 @@ export const TimeLimitsManager = GObject.registerClass({ this._parentalControlsManager = this._parentalControlsManagerFactory.new(); this._parentalControlsManager.connectObject( - 'session-limits-changed', () => this._onSessionLimitsChanged().catch(logError), - this); + 'session-limits-changed', () => { + this._onSessionLimitsChanged().catch(logError); + this.notify('daily-limit-time'); + this.notify('daily-limit-enabled'); + }, this); this._estimatedTimes = []; this._timerChildProxyFactory = timerChildProxyFactory ?? { @@ -295,6 +299,7 @@ export const TimeLimitsManager = GObject.registerClass({ this._estimatedTimes = []; this._updateState(); + this.notify('daily-limit-time'); } _storingTransitionsEnabled() { @@ -866,17 +871,17 @@ export const TimeLimitsManager = GObject.registerClass({ } /** - * Work out the timestamp at which the daily limit was reached. + * Work out the timestamp at which the wellbeing daily limit was reached. * - * If the user has not reached the daily limit yet today, this will return 0. + * If the user has not reached the wellbeing daily limit yet today, this will return 0. * * @param {number} nowSecs ‘Current’ time to calculate from. - * @param {number} dailyLimitSecs Daily limit in seconds. + * @param {number} wellbeingDailyLimitSecs Wellbeing daily limit in seconds. * @param {number} startOfTodaySecs Time for the start of today. * @returns {number} */ - _calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs) { - console.assert(this.dailyLimitEnabled, + _calculateWellbeingDailyLimitReachedAtSecs(nowSecs, wellbeingDailyLimitSecs, startOfTodaySecs) { + console.assert(this.wellbeingDailyLimitEnabled, 'Daily limit reached-at time only makes sense if limits are enabled'); // NOTE: This might return -1. @@ -892,16 +897,16 @@ export const TimeLimitsManager = GObject.registerClass({ else if (this._stateTransitions[i]['oldState'] === UserState.ACTIVE) activeTimeTodaySecs += Math.max(this._stateTransitions[i]['wallTimeSecs'] - activeStartTimeSecs, 0); - if (activeTimeTodaySecs >= dailyLimitSecs) - return this._stateTransitions[i]['wallTimeSecs'] - (activeTimeTodaySecs - dailyLimitSecs); + if (activeTimeTodaySecs >= wellbeingDailyLimitSecs) + return this._stateTransitions[i]['wallTimeSecs'] - (activeTimeTodaySecs - wellbeingDailyLimitSecs); } if (this._stateTransitions.length > 0 && this._stateTransitions.at(-1)['newState'] === UserState.ACTIVE) activeTimeTodaySecs += Math.max(nowSecs - activeStartTimeSecs, 0); - if (activeTimeTodaySecs >= dailyLimitSecs) - return nowSecs - (activeTimeTodaySecs - dailyLimitSecs); + if (activeTimeTodaySecs >= wellbeingDailyLimitSecs) + return nowSecs - (activeTimeTodaySecs - wellbeingDailyLimitSecs); // Limit not reached yet. return 0; @@ -931,10 +936,11 @@ export const TimeLimitsManager = GObject.registerClass({ if (startOfTodaySecs > this._lastStateChangeTimeSecs) newState = TimeLimitsState.ACTIVE; + // Work out estimated times for the parental controls session limits calculations. const [, currentSessionStart, currentSessionEnd, nextSessionStart] = this._estimatedTimes; - const sessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled(); + const parentalControlsSessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled(); - if (sessionLimitsEnabled) { + if (parentalControlsSessionLimitsEnabled) { // 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. @@ -942,25 +948,26 @@ export const TimeLimitsManager = GObject.registerClass({ return; } - const sessionLimitsDebug = sessionLimitsEnabled ? 'current session start: ' + + const parentalControlsSessionLimitsDebug = parentalControlsSessionLimitsEnabled ? 'current session start: ' + `${this._unixToString(currentSessionStart)}, current session end: ` + `${this._unixToString(currentSessionEnd)}` : 'disabled'; console.debug('TimeLimitsManager: Parental controls session limits: ' + - `${sessionLimitsDebug}`); + `${parentalControlsSessionLimitsDebug}`); - // 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 + // for the wellbeing daily limit calculations. 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 wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitEnabled = this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); - const dailyLimitDebug = dailyLimitEnabled ? `${dailyLimitSecs}s` : 'disabled'; + const wellbeingDailyLimitDebug = wellbeingDailyLimitEnabled ? `${wellbeingDailyLimitSecs}s` : 'disabled'; console.debug('TimeLimitsManager: Wellbeing active time today: ' + - `${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`); + `${activeTimeTodaySecs}s, daily limit ${wellbeingDailyLimitDebug}`); // Update TimeLimitsState. When the user is inactive, there's no point // scheduling anything until they become active again. - if (sessionLimitsEnabled) { + if (parentalControlsSessionLimitsEnabled) { if (nextSessionStart <= nowSecs) { // Just entered daily schedule or a new day, so update cached // estimated times, which will perform state update afterwards. @@ -979,7 +986,7 @@ export const TimeLimitsManager = GObject.registerClass({ // Schedule an update for when we expect the session limit will be reached. this._scheduleUpdateState(currentSessionEnd - nowSecs); } - } else if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) { + } else if (wellbeingDailyLimitEnabled && activeTimeTodaySecs >= wellbeingDailyLimitSecs) { newState = TimeLimitsState.LIMIT_REACHED; // Schedule an update for when the daily limit will be reset again. @@ -987,9 +994,9 @@ export const TimeLimitsManager = GObject.registerClass({ } else if (this._userState === UserState.ACTIVE) { newState = TimeLimitsState.ACTIVE; - // Schedule an update for when we expect the daily limit will be reached. - if (dailyLimitEnabled) - this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs); + // Schedule an update for when we expect the wellbeing daily limit will be reached. + if (wellbeingDailyLimitEnabled) + this._scheduleUpdateState(wellbeingDailyLimitSecs - activeTimeTodaySecs); } else { // User is inactive, so no point scheduling anything until they become // active again. @@ -1041,10 +1048,19 @@ export const TimeLimitsManager = GObject.registerClass({ * * @type {boolean} */ - get sessionLimitsEnabled() { + get parentalControlsSessionLimitsEnabled() { return this._parentalControlsManager.sessionLimitsEnabled(); } + /** + * Whether the wellbeing daily limit is enabled. + * + * @type {boolean} + */ + get wellbeingDailyLimitEnabled() { + return this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); + } + /** * 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 @@ -1053,38 +1069,53 @@ export const TimeLimitsManager = GObject.registerClass({ * limit, or if time limits are disabled, this is zero. * It’s measured in real time seconds. * + * Considers both the parental controls and wellbeing limits. + * * @type {number} */ get dailyLimitTime() { + // Check parental controls session limits + if (this.parentalControlsSessionLimitsEnabled) { + if (this._state === TimeLimitsState.DISABLED || + this._userState === UserState.INACTIVE) + return 0; + + const [, , currentSessionEnd] = this._estimatedTimes; + return currentSessionEnd; + } + + // Handle wellbeing daily limit otherwise switch (this._state) { case TimeLimitsState.DISABLED: return 0; case TimeLimitsState.ACTIVE: { - if (!this.dailyLimitEnabled) + if (!this.wellbeingDailyLimitEnabled) return 0; const nowSecs = this.getCurrentTime(); const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs); const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs); - const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); - console.assert(dailyLimitSecs >= activeTimeTodaySecs, 'Active time unexpectedly high'); + console.assert(wellbeingDailyLimitSecs >= activeTimeTodaySecs, 'Active time unexpectedly high'); if (this._userState === UserState.ACTIVE) - return nowSecs + (dailyLimitSecs - activeTimeTodaySecs); + return nowSecs + (wellbeingDailyLimitSecs - activeTimeTodaySecs); else return 0; } case TimeLimitsState.LIMIT_REACHED: { const nowSecs = this.getCurrentTime(); const [startOfTodaySecs] = this._getStartOfTodaySecs(nowSecs); - const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); - const dailyLimitReachedAtSecs = this._calculateDailyLimitReachedAtSecs(nowSecs, dailyLimitSecs, startOfTodaySecs); + const wellbeingDailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds'); + const wellbeingDailyLimitReachedAtSecs = + this._calculateWellbeingDailyLimitReachedAtSecs(nowSecs, + wellbeingDailyLimitSecs, startOfTodaySecs); - console.assert(dailyLimitReachedAtSecs > 0, + console.assert(wellbeingDailyLimitReachedAtSecs > 0, 'Daily limit reached-at unexpectedly low'); - return dailyLimitReachedAtSecs; + return wellbeingDailyLimitReachedAtSecs; } default: console.assert(false, `Unexpected state ${this._state}`); @@ -1095,10 +1126,13 @@ export const TimeLimitsManager = GObject.registerClass({ /** * Whether the daily limit is enabled. * + * Considers both the parental controls and wellbeing limits. + * * @type {boolean} */ get dailyLimitEnabled() { - return this._screenTimeLimitSettings.get_boolean('daily-limit-enabled'); + return this.parentalControlsSessionLimitsEnabled || + this.wellbeingDailyLimitEnabled; } /** @@ -1188,7 +1222,7 @@ class TimeLimitsDispatcher extends GObject.Object { } case TimeLimitsState.LIMIT_REACHED: { - if (this._manager.sessionLimitsEnabled) { + if (this._manager.parentalControlsSessionLimitsEnabled) { // 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 @@ -1336,26 +1370,46 @@ class TimeLimitsNotificationSource extends GObject.Object { const remainingSecs = limitDueTime - currentTime; console.debug(`TimeLimitsNotificationSource: ${remainingSecs}s left before limit is reached`); - if (remainingSecs > LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS) { + let timeoutLimit; + + if (this._manager.parentalControlsSessionLimitsEnabled) + timeoutLimit = PARENTAL_CONTROLS_LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS; + else + timeoutLimit = LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS; + + if (remainingSecs > timeoutLimit) { this._notification?.destroy(); this._notification = null; // Schedule to show a notification when the upcoming notification // time is reached. - this._scheduleUpdateState(remainingSecs - LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS); + this._scheduleUpdateState(remainingSecs - timeoutLimit); break; - } else if (Math.ceil(remainingSecs) === LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS) { - // Bang on time to show this notification. - const remainingMinutes = Math.floor(LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS / 60); - const titleText = Gettext.ngettext( - 'Screen Time Limit in %d Minute', - 'Screen Time Limit in %d Minutes', - remainingMinutes - ).format(remainingMinutes); + } else if (Math.ceil(remainingSecs) === timeoutLimit) { + let remainingTime, titleText, bodyText; + + if (this._manager.parentalControlsSessionLimitsEnabled) { + remainingTime = timeoutLimit; + titleText = _('Screen Time Limit is almost up'); + bodyText = Gettext.ngettext( + 'The computer will lock in %d second.', + 'The computer will lock in %d seconds.', + remainingTime + ).format(remainingTime); + } else { + // Bang on time to show this notification. + remainingTime = Math.floor(timeoutLimit / 60); + titleText = Gettext.ngettext( + 'Screen Time Limit in %d Minute', + 'Screen Time Limit in %d Minutes', + remainingTime + ).format(remainingTime); + bodyText = _('Your screen time limit is approaching'); + } this._ensureNotification({ title: titleText, - body: _('Your screen time limit is approaching'), + body: bodyText, urgency: MessageTray.Urgency.HIGH, }); this._source.addNotification(this._notification); @@ -1368,7 +1422,7 @@ class TimeLimitsNotificationSource extends GObject.Object { // Notify the user that they’ve reached their limit, when we // transition from any state to LIMIT_REACHED. Noop if parental // controls session limits are enabled, since we’re locking then. - if (!this._manager.sessionLimitsEnabled && + if (!this._manager.parentalControlsSessionLimitsEnabled && this._previousState !== TimeLimitsState.LIMIT_REACHED) { this._ensureNotification({ title: _('Screen Time Limit Reached'),