Skip to content

Commit 58aa70b

Browse files
sofarclaude
andcommitted
Add quiet hours (scheduled notification silencing)
Automatically switches notification status to Sleep at a configurable start hour and back at a configurable end hour. Includes a new settings screen with enable toggle and start/end hour controls. Disabled by default (21:00-09:00). Also adds moon glyph to the default UI font so it displays in the settings menu. This feature draws on extensive community discussion across several related PRs. The following feedback was reviewed and incorporated: Adopted: - "Quiet hours" naming (kieranc, PR InfiniTimeOrg#1461): renamed from "sleep setting" / "auto sleep" to avoid confusion with the existing sleep mode. - Preserve notification state across transitions (Itai-Nelken, PR InfiniTimeOrg#1461): quiet hours now saves the user's notification status (On/Off/Sleep) before entering and restores it when exiting, instead of unconditionally forcing On. Uses transient (non-persisted) state following the existing bleRadioEnabled/dfuAndFsEnabledTillReboot pattern. - Previous state was Sleep edge case (FintasticMan, PR InfiniTimeOrg#1461): if the previous state was already Sleep when quiet hours began, it is restored faithfully. FintasticMan suggested restoring to Off instead, but preserving the actual state is more predictable and consistent. - Alarm overrides quiet hours (FintasticMan, PR InfiniTimeOrg#1461): when an alarm fires, quiet hours are exited so the alarm can wake the user. This ensures alarms are never silenced by scheduled quiet hours. - Disable wrist-lower-to-sleep during sleep mode (kieranc, PR InfiniTimeOrg#2415, approved by NeroBurner): wrist-raise wake was already suppressed during sleep mode but wrist-lower-to-sleep was not, which is inconsistent. Moved the lower-wrist check inside the existing != Sleep guard per mark9064's code review suggestion to avoid a duplicate condition check. - Respect explicit user choices (chmeeedalf, PR InfiniTimeOrg#2002): if a user manually changes notification status via QuickSettings during quiet hours, that works normally; the original pre-quiet-hours state is still restored when quiet hours end. - Chimes suppressed during quiet hours: the existing chime handlers already gate on notificationStatus != Sleep, so setting Sleep during quiet hours suppresses chimes automatically with no additional code. Not adopted: - Separate auto-start/auto-stop toggles (Boteium, PR InfiniTimeOrg#1461): would let a user manually enter sleep early but still auto-wake. Adds UI complexity for a niche use case; a single toggle is simpler and aligns with the InfiniTime vision of "prefer solid defaults over customisability" (mark9064, PR InfiniTimeOrg#2230). - Sleep Bluetooth checkbox (escoand, PR InfiniTimeOrg#1461): BLE control during sleep is a separate security concern that deserves its own feature, not a quiet hours sub-option (mark9064, PR InfiniTimeOrg#2230). - Configurable sleep mode behaviors -- AOD, chimes, notifications, step tracking (JustScott, PR InfiniTimeOrg#2230): maintainer mark9064 noted that sleep mode means the user is sleeping, so allowing notifications/chimes/AOD contradicts its purpose. The author agreed this belongs in forks, not mainline. - Red/dim screen during sleep (minacode/lman0, PR InfiniTimeOrg#1261): a larger UX change outside the scope of notification scheduling. - Vibration priority system (minacode, PR InfiniTimeOrg#1328): a proper priority queue (phone > timer > alarm > notification) would be ideal for centralized DND management, but requires a motor controller rework that is a much larger effort. - 30-minute or 15-minute granularity for quiet hours times (LinuxinaBit, PR InfiniTimeOrg#1461; zischknall, PR InfiniTimeOrg#2227): hour granularity is sufficient for scheduling sleep/wake times and keeps the UI simple. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b8b2c3c commit 58aa70b

9 files changed

Lines changed: 276 additions & 2 deletions

File tree

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ list(APPEND SOURCE_FILES
416416
displayapp/screens/settings/SettingShakeThreshold.cpp
417417
displayapp/screens/settings/SettingBluetooth.cpp
418418
displayapp/screens/settings/SettingOTA.cpp
419+
displayapp/screens/settings/SettingQuietHours.cpp
419420

420421
## Watch faces
421422
displayapp/screens/WatchFaceAnalog.cpp

src/components/settings/Settings.h

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,64 @@ namespace Pinetime {
351351
settings.heartRateBackgroundPeriod = newIntervalInSeconds.value();
352352
}
353353

354+
bool GetQuietHoursEnabled() const {
355+
return settings.quietHoursEnabled;
356+
}
357+
358+
void SetQuietHoursEnabled(bool enabled) {
359+
if (enabled != settings.quietHoursEnabled) {
360+
settingsChanged = true;
361+
}
362+
settings.quietHoursEnabled = enabled;
363+
}
364+
365+
uint8_t GetQuietHoursStart() const {
366+
return settings.quietHoursStart;
367+
}
368+
369+
void SetQuietHoursStart(uint8_t hour) {
370+
if (hour != settings.quietHoursStart) {
371+
settingsChanged = true;
372+
}
373+
settings.quietHoursStart = hour;
374+
}
375+
376+
uint8_t GetQuietHoursEnd() const {
377+
return settings.quietHoursEnd;
378+
}
379+
380+
void SetQuietHoursEnd(uint8_t hour) {
381+
if (hour != settings.quietHoursEnd) {
382+
settingsChanged = true;
383+
}
384+
settings.quietHoursEnd = hour;
385+
}
386+
387+
void EnterQuietHours() {
388+
if (!settings.inQuietHours) {
389+
settings.notificationStatusBeforeQuietHours = settings.notificationStatus;
390+
settings.inQuietHours = true;
391+
settingsChanged = true;
392+
SetNotificationStatus(Notification::Sleep);
393+
}
394+
}
395+
396+
void ExitQuietHours() {
397+
if (settings.inQuietHours) {
398+
settings.inQuietHours = false;
399+
settingsChanged = true;
400+
SetNotificationStatus(settings.notificationStatusBeforeQuietHours);
401+
}
402+
}
403+
404+
bool IsInQuietHours() const {
405+
return settings.inQuietHours;
406+
}
407+
354408
private:
355409
Pinetime::Controllers::FS& fs;
356410

357-
static constexpr uint32_t settingsVersion = 0x000a;
411+
static constexpr uint32_t settingsVersion = 0x000c;
358412

359413
struct SettingsData {
360414
uint32_t version = settingsVersion;
@@ -383,6 +437,13 @@ namespace Pinetime {
383437

384438
bool dfuAndFsEnabledOnBoot = false;
385439
uint16_t heartRateBackgroundPeriod = std::numeric_limits<uint16_t>::max(); // Disabled by default
440+
441+
bool quietHoursEnabled = false;
442+
uint8_t quietHoursStart = 21; // 9 PM
443+
uint8_t quietHoursEnd = 9; // 9 AM
444+
445+
bool inQuietHours = false;
446+
Notification notificationStatusBeforeQuietHours = Notification::On;
386447
};
387448

388449
SettingsData settings;

src/displayapp/DisplayApp.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "displayapp/screens/settings/SettingShakeThreshold.h"
4949
#include "displayapp/screens/settings/SettingBluetooth.h"
5050
#include "displayapp/screens/settings/SettingOTA.h"
51+
#include "displayapp/screens/settings/SettingQuietHours.h"
5152

5253
#include "utility/Math.h"
5354

@@ -629,6 +630,9 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio
629630
case Apps::SettingOTA:
630631
currentScreen = std::make_unique<Screens::SettingOTA>(this, settingsController);
631632
break;
633+
case Apps::SettingQuietHours:
634+
currentScreen = std::make_unique<Screens::SettingQuietHours>(settingsController);
635+
break;
632636
case Apps::BatteryInfo:
633637
currentScreen = std::make_unique<Screens::BatteryInfo>(batteryController);
634638
break;

src/displayapp/apps/Apps.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ namespace Pinetime {
4141
SettingShakeThreshold,
4242
SettingBluetooth,
4343
SettingOTA,
44+
SettingQuietHours,
4445
Error
4546
};
4647

src/displayapp/fonts/fonts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
{
99
"file": "FontAwesome5-Solid+Brands+Regular.woff",
10-
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed"
10+
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf186, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed"
1111
}
1212
],
1313
"bpp": 1,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#include "displayapp/screens/settings/SettingQuietHours.h"
2+
#include <lvgl/lvgl.h>
3+
#include "displayapp/DisplayApp.h"
4+
#include "displayapp/screens/Symbols.h"
5+
#include "displayapp/InfiniTimeTheme.h"
6+
7+
using namespace Pinetime::Applications::Screens;
8+
9+
namespace {
10+
void event_handler(lv_obj_t* obj, lv_event_t event) {
11+
auto* screen = static_cast<SettingQuietHours*>(obj->user_data);
12+
screen->UpdateSelected(obj, event);
13+
}
14+
15+
void checkbox_event_handler(lv_obj_t* obj, lv_event_t event) {
16+
if (event == LV_EVENT_VALUE_CHANGED) {
17+
auto* screen = static_cast<SettingQuietHours*>(obj->user_data);
18+
screen->ToggleEnabled();
19+
}
20+
}
21+
}
22+
23+
SettingQuietHours::SettingQuietHours(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} {
24+
25+
lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr);
26+
lv_label_set_text_static(title, "Quiet hours");
27+
lv_label_set_align(title, LV_LABEL_ALIGN_CENTER);
28+
lv_obj_align(title, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 15, 15);
29+
30+
lv_obj_t* icon = lv_label_create(lv_scr_act(), nullptr);
31+
lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
32+
lv_label_set_text_static(icon, Symbols::moon);
33+
lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER);
34+
lv_obj_align(icon, title, LV_ALIGN_OUT_LEFT_MID, -10, 0);
35+
36+
enabledCheckbox = lv_checkbox_create(lv_scr_act(), nullptr);
37+
lv_checkbox_set_text(enabledCheckbox, "Enabled");
38+
lv_checkbox_set_checked(enabledCheckbox, settingsController.GetQuietHoursEnabled());
39+
enabledCheckbox->user_data = this;
40+
lv_obj_set_event_cb(enabledCheckbox, checkbox_event_handler);
41+
lv_obj_align(enabledCheckbox, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 10, 55);
42+
43+
static constexpr uint8_t btnWidth = 50;
44+
static constexpr uint8_t btnHeight = 40;
45+
46+
// Start hour row
47+
lv_obj_t* startLabel = lv_label_create(lv_scr_act(), nullptr);
48+
lv_label_set_text_static(startLabel, "Start");
49+
lv_obj_align(startLabel, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 10, -15);
50+
51+
btnStartMinus = lv_btn_create(lv_scr_act(), nullptr);
52+
btnStartMinus->user_data = this;
53+
lv_obj_set_size(btnStartMinus, btnWidth, btnHeight);
54+
lv_obj_align(btnStartMinus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 70, -15);
55+
lv_obj_set_style_local_bg_color(btnStartMinus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
56+
lv_obj_t* lblStartMinus = lv_label_create(btnStartMinus, nullptr);
57+
lv_label_set_text_static(lblStartMinus, "-");
58+
lv_obj_set_event_cb(btnStartMinus, event_handler);
59+
60+
startValue = lv_label_create(lv_scr_act(), nullptr);
61+
lv_obj_set_style_local_text_font(startValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
62+
lv_label_set_text_fmt(startValue, "%02d:00", settingsController.GetQuietHoursStart());
63+
lv_obj_align(startValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 132, -15);
64+
65+
btnStartPlus = lv_btn_create(lv_scr_act(), nullptr);
66+
btnStartPlus->user_data = this;
67+
lv_obj_set_size(btnStartPlus, btnWidth, btnHeight);
68+
lv_obj_align(btnStartPlus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 195, -15);
69+
lv_obj_set_style_local_bg_color(btnStartPlus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
70+
lv_obj_t* lblStartPlus = lv_label_create(btnStartPlus, nullptr);
71+
lv_label_set_text_static(lblStartPlus, "+");
72+
lv_obj_set_event_cb(btnStartPlus, event_handler);
73+
74+
// End hour row
75+
lv_obj_t* endLabel = lv_label_create(lv_scr_act(), nullptr);
76+
lv_label_set_text_static(endLabel, "End");
77+
lv_obj_align(endLabel, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 10, 40);
78+
79+
btnEndMinus = lv_btn_create(lv_scr_act(), nullptr);
80+
btnEndMinus->user_data = this;
81+
lv_obj_set_size(btnEndMinus, btnWidth, btnHeight);
82+
lv_obj_align(btnEndMinus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 70, 40);
83+
lv_obj_set_style_local_bg_color(btnEndMinus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
84+
lv_obj_t* lblEndMinus = lv_label_create(btnEndMinus, nullptr);
85+
lv_label_set_text_static(lblEndMinus, "-");
86+
lv_obj_set_event_cb(btnEndMinus, event_handler);
87+
88+
endValue = lv_label_create(lv_scr_act(), nullptr);
89+
lv_obj_set_style_local_text_font(endValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
90+
lv_label_set_text_fmt(endValue, "%02d:00", settingsController.GetQuietHoursEnd());
91+
lv_obj_align(endValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 132, 40);
92+
93+
btnEndPlus = lv_btn_create(lv_scr_act(), nullptr);
94+
btnEndPlus->user_data = this;
95+
lv_obj_set_size(btnEndPlus, btnWidth, btnHeight);
96+
lv_obj_align(btnEndPlus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 195, 40);
97+
lv_obj_set_style_local_bg_color(btnEndPlus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
98+
lv_obj_t* lblEndPlus = lv_label_create(btnEndPlus, nullptr);
99+
lv_label_set_text_static(lblEndPlus, "+");
100+
lv_obj_set_event_cb(btnEndPlus, event_handler);
101+
}
102+
103+
SettingQuietHours::~SettingQuietHours() {
104+
lv_obj_clean(lv_scr_act());
105+
settingsController.SaveSettings();
106+
}
107+
108+
void SettingQuietHours::ToggleEnabled() {
109+
bool wasEnabled = settingsController.GetQuietHoursEnabled();
110+
settingsController.SetQuietHoursEnabled(!wasEnabled);
111+
if (wasEnabled && settingsController.IsInQuietHours()) {
112+
settingsController.ExitQuietHours();
113+
}
114+
lv_checkbox_set_checked(enabledCheckbox, settingsController.GetQuietHoursEnabled());
115+
}
116+
117+
void SettingQuietHours::UpdateSelected(lv_obj_t* object, lv_event_t event) {
118+
if (event != LV_EVENT_SHORT_CLICKED && event != LV_EVENT_LONG_PRESSED_REPEAT) {
119+
return;
120+
}
121+
122+
if (object == btnStartPlus) {
123+
uint8_t val = settingsController.GetQuietHoursStart();
124+
val = (val + 1) % 24;
125+
settingsController.SetQuietHoursStart(val);
126+
lv_label_set_text_fmt(startValue, "%02d:00", val);
127+
lv_obj_realign(startValue);
128+
} else if (object == btnStartMinus) {
129+
uint8_t val = settingsController.GetQuietHoursStart();
130+
val = (val + 23) % 24;
131+
settingsController.SetQuietHoursStart(val);
132+
lv_label_set_text_fmt(startValue, "%02d:00", val);
133+
lv_obj_realign(startValue);
134+
} else if (object == btnEndPlus) {
135+
uint8_t val = settingsController.GetQuietHoursEnd();
136+
val = (val + 1) % 24;
137+
settingsController.SetQuietHoursEnd(val);
138+
lv_label_set_text_fmt(endValue, "%02d:00", val);
139+
lv_obj_realign(endValue);
140+
} else if (object == btnEndMinus) {
141+
uint8_t val = settingsController.GetQuietHoursEnd();
142+
val = (val + 23) % 24;
143+
settingsController.SetQuietHoursEnd(val);
144+
lv_label_set_text_fmt(endValue, "%02d:00", val);
145+
lv_obj_realign(endValue);
146+
}
147+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <lvgl/lvgl.h>
5+
#include "components/settings/Settings.h"
6+
#include "displayapp/screens/Screen.h"
7+
8+
namespace Pinetime {
9+
10+
namespace Applications {
11+
namespace Screens {
12+
13+
class SettingQuietHours : public Screen {
14+
public:
15+
SettingQuietHours(Pinetime::Controllers::Settings& settingsController);
16+
~SettingQuietHours() override;
17+
18+
void UpdateSelected(lv_obj_t* object, lv_event_t event);
19+
void ToggleEnabled();
20+
21+
private:
22+
Controllers::Settings& settingsController;
23+
24+
lv_obj_t* enabledCheckbox;
25+
lv_obj_t* startValue;
26+
lv_obj_t* endValue;
27+
lv_obj_t* btnStartPlus;
28+
lv_obj_t* btnStartMinus;
29+
lv_obj_t* btnEndPlus;
30+
lv_obj_t* btnEndMinus;
31+
};
32+
}
33+
}
34+
}

src/displayapp/screens/settings/Settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ namespace Pinetime {
4848

4949
{Symbols::shieldAlt, "Over-the-air", Apps::SettingOTA},
5050
{Symbols::bluetooth, "Bluetooth", Apps::SettingBluetooth},
51+
{Symbols::moon, "Quiet hours", Apps::SettingQuietHours},
5152
{Symbols::list, "About", Apps::SysInfo},
5253
}};
5354
ScreenList<nScreens> screens;

src/systemtask/SystemTask.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,22 @@ void SystemTask::Work() {
224224
if (alarmController.IsEnabled()) {
225225
alarmController.ScheduleAlarm();
226226
}
227+
if (settingsController.GetQuietHoursEnabled()) {
228+
uint8_t currentHour = dateTimeController.Hours();
229+
uint8_t start = settingsController.GetQuietHoursStart();
230+
uint8_t end = settingsController.GetQuietHoursEnd();
231+
bool shouldBeInQuietHours;
232+
if (start <= end) {
233+
shouldBeInQuietHours = (currentHour >= start && currentHour < end);
234+
} else {
235+
shouldBeInQuietHours = (currentHour >= start || currentHour < end);
236+
}
237+
if (shouldBeInQuietHours) {
238+
settingsController.EnterQuietHours();
239+
} else {
240+
settingsController.ExitQuietHours();
241+
}
242+
}
227243
break;
228244
case Messages::OnNewNotification:
229245
if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On) {
@@ -234,6 +250,7 @@ void SystemTask::Work() {
234250
}
235251
break;
236252
case Messages::SetOffAlarm:
253+
settingsController.ExitQuietHours();
237254
GoToRunning();
238255
displayApp.PushMessage(Pinetime::Applications::Display::Messages::AlarmTriggered);
239256
break;
@@ -348,6 +365,14 @@ void SystemTask::Work() {
348365
break;
349366
case Messages::OnNewHour:
350367
using Pinetime::Controllers::AlarmController;
368+
if (settingsController.GetQuietHoursEnabled()) {
369+
uint8_t currentHour = dateTimeController.Hours();
370+
if (currentHour == settingsController.GetQuietHoursStart()) {
371+
settingsController.EnterQuietHours();
372+
} else if (currentHour == settingsController.GetQuietHoursEnd()) {
373+
settingsController.ExitQuietHours();
374+
}
375+
}
351376
if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep &&
352377
settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::Hours && !alarmController.IsAlerting()) {
353378
GoToRunning();

0 commit comments

Comments
 (0)