Files
pebble/src/fw/services/normal/notifications/alerts_preferences.c
T

476 lines
18 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "services/normal/notifications/alerts_preferences.h"
#include "services/normal/notifications/alerts_preferences_private.h"
#include "drivers/rtc.h"
#include "popups/notifications/notification_window.h"
#include "services/common/analytics/analytics.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/vibes/vibe_intensity.h"
#include "system/passert.h"
#include "os/mutex.h"
#include "util/bitset.h"
#include <string.h>
#define FILE_NAME "notifpref"
#define FILE_LEN (1024)
static PebbleMutex *s_mutex;
///////////////////////////////////
//! Preference keys
///////////////////////////////////
#define PREF_KEY_MASK "mask"
static AlertMask s_mask = AlertMaskAllOn;
#define PREF_KEY_DND_INTERRUPTIONS_MASK "dndInterruptionsMask"
static AlertMask s_dnd_interruptions_mask = AlertMaskAllOff;
#define PREF_KEY_VIBE "vibe"
static bool s_vibe_on_notification = true;
#define PREF_KEY_VIBE_INTENSITY "vibeIntensity"
static VibeIntensity s_vibe_intensity = DEFAULT_VIBE_INTENSITY;
#if CAPABILITY_HAS_VIBE_SCORES
#define PREF_KEY_VIBE_SCORE_NOTIFICATIONS ("vibeScoreNotifications")
static VibeScoreId s_vibe_score_notifications = DEFAULT_VIBE_SCORE_NOTIFS;
#define PREF_KEY_VIBE_SCORE_INCOMING_CALLS ("vibeScoreIncomingCalls")
static VibeScoreId s_vibe_score_incoming_calls = DEFAULT_VIBE_SCORE_INCOMING_CALLS;
#define PREF_KEY_VIBE_SCORE_ALARMS ("vibeScoreAlarms")
static VibeScoreId s_vibe_score_alarms = DEFAULT_VIBE_SCORE_ALARMS;
#endif
#define PREF_KEY_DND_MANUALLY_ENABLED "dndManuallyEnabled"
static bool s_do_not_disturb_manually_enabled = false;
#define PREF_KEY_DND_SMART_ENABLED "dndSmartEnabled"
static bool s_do_not_disturb_smart_dnd_enabled = false;
#define PREF_KEY_FIRST_USE_COMPLETE "firstUseComplete"
static uint32_t s_first_use_complete = 0;
#define PREF_KEY_NOTIF_WINDOW_TIMEOUT "notifWindowTimeout"
static uint32_t s_notif_window_timeout_ms = NOTIF_WINDOW_TIMEOUT_DEFAULT;
///////////////////////////////////
//! Legacy preference keys
///////////////////////////////////
#define PREF_KEY_LEGACY_DND_SCHEDULE "dndSchedule"
static DoNotDisturbSchedule s_legacy_dnd_schedule = {
.from_hour = 0,
.to_hour = 6,
};
#define PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED "dndEnabled"
static bool s_legacy_dnd_schedule_enabled = false;
#define PREF_KEY_LEGACY_DND_MANUAL_FIRST_USE "dndManualFirstUse"
#define PREF_KEY_LEGACY_DND_SMART_FIRST_USE "dndSmartFirstUse"
///////////////////////////////////
//! Variables
///////////////////////////////////
typedef struct DoNotDisturbScheduleConfig {
DoNotDisturbSchedule schedule;
bool enabled;
} DoNotDisturbScheduleConfig;
typedef struct DoNotDisturbScheduleConfigKeys {
const char *schedule_pref_key;
const char *enabled_pref_key;
} DoNotDisturbScheduleConfigKeys;
static DoNotDisturbScheduleConfig s_dnd_schedule[NumDNDSchedules];
static const DoNotDisturbScheduleConfigKeys s_dnd_schedule_keys[NumDNDSchedules] = {
[WeekdaySchedule] = {
.schedule_pref_key = "dndWeekdaySchedule",
.enabled_pref_key = "dndWeekdayScheduleEnabled",
},
[WeekendSchedule] = {
.schedule_pref_key = "dndWeekendSchedule",
.enabled_pref_key = "dndWeekendScheduleEnabled",
}
};
static void prv_migrate_legacy_dnd_schedule(SettingsFile *file) {
// If Weekday schedule does not exist, assume that the other 3 settings files are missing as well
// Set the new schedules to the legacy schedule and delete the legacy schedule
if (!settings_file_exists(file, s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
strlen(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key))) {
#define SET_PREF_ALREADY_OPEN(key, value) \
settings_file_set(file, key, strlen(key), value, sizeof(value));
s_dnd_schedule[WeekdaySchedule].schedule = s_legacy_dnd_schedule;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
&s_dnd_schedule[WeekdaySchedule].schedule);
s_dnd_schedule[WeekdaySchedule].enabled = s_legacy_dnd_schedule_enabled;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekdaySchedule].enabled_pref_key,
&s_dnd_schedule[WeekdaySchedule].enabled);
s_dnd_schedule[WeekendSchedule].schedule = s_legacy_dnd_schedule;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekendSchedule].schedule_pref_key,
&s_dnd_schedule[WeekendSchedule].schedule);
s_dnd_schedule[WeekendSchedule].enabled = s_legacy_dnd_schedule_enabled;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekendSchedule].enabled_pref_key,
&s_dnd_schedule[WeekendSchedule].enabled);
#undef SET_PREF_ALREADY_OPEN
#define DELETE_PREF(key) \
do { \
if (settings_file_exists(file, key, strlen(key))) { \
settings_file_delete(file, key, strlen(key)); \
} \
} while (0)
DELETE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE);
DELETE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED);
#undef DELETE_PREF
}
}
#if !PLATFORM_TINTIN
static void prv_migrate_legacy_first_use_settings(SettingsFile *file) {
// These don't need to be initialized since settings_file_get will clear them on error
uint8_t manual_dnd_first_use_complete;
bool smart_dnd_first_use_complete;
// Migrate the old first use dialog prefs
#define RESTORE_AND_DELETE_PREF(key, var) \
do { \
if (settings_file_get(file, key, strlen(key), &var, sizeof(var)) == S_SUCCESS) { \
settings_file_delete(file, key, strlen(key)); \
} \
} while (0)
RESTORE_AND_DELETE_PREF(PREF_KEY_LEGACY_DND_MANUAL_FIRST_USE, manual_dnd_first_use_complete);
RESTORE_AND_DELETE_PREF(PREF_KEY_LEGACY_DND_SMART_FIRST_USE, smart_dnd_first_use_complete);
s_first_use_complete |= manual_dnd_first_use_complete << FirstUseSourceManualDNDActionMenu;
s_first_use_complete |= smart_dnd_first_use_complete << FirstUseSourceSmartDND;
#undef RESTORE_AND_DELETE_PREF
}
#endif
#if CAPABILITY_HAS_VIBE_SCORES
static void prv_save_all_vibe_scores_to_file(SettingsFile *file) {
#define SET_PREF_ALREADY_OPEN(key, value) \
settings_file_set(file, key, strlen(key), &value, sizeof(value));
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_NOTIFICATIONS, s_vibe_score_notifications);
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_INCOMING_CALLS, s_vibe_score_incoming_calls);
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_ALARMS, s_vibe_score_alarms);
#undef SET_PREF_ALREADY_OPEN
}
static VibeScoreId prv_return_default_if_invalid(VibeScoreId id, VibeScoreId default_id) {
return vibe_score_info_is_valid(id) ? id : default_id;
}
// Uses the default vibe pattern id if the given score isn't valid
static void prv_ensure_valid_vibe_scores(void) {
s_vibe_score_notifications = prv_return_default_if_invalid(s_vibe_score_notifications,
DEFAULT_VIBE_SCORE_NOTIFS);
s_vibe_score_incoming_calls = prv_return_default_if_invalid(s_vibe_score_incoming_calls,
DEFAULT_VIBE_SCORE_INCOMING_CALLS);
s_vibe_score_alarms = prv_return_default_if_invalid(s_vibe_score_alarms,
DEFAULT_VIBE_SCORE_ALARMS);
}
static void prv_set_vibe_scores_based_on_legacy_intensity(VibeIntensity intensity) {
if (intensity == VibeIntensityHigh) {
s_vibe_score_notifications = VibeScoreId_StandardShortPulseHigh;
s_vibe_score_incoming_calls = VibeScoreId_StandardLongPulseHigh;
s_vibe_score_alarms = VibeScoreId_StandardLongPulseHigh;
} else {
s_vibe_score_notifications = VibeScoreId_StandardShortPulseLow;
s_vibe_score_incoming_calls = VibeScoreId_StandardLongPulseLow;
s_vibe_score_alarms = VibeScoreId_StandardLongPulseLow;
}
}
static void prv_migrate_vibe_intensity_to_vibe_scores(SettingsFile *file) {
// We use the existence of the notifications vibe score pref as a shallow measurement of whether
// or not the user has migrated to vibe scores
const bool user_has_migrated_to_vibe_scores =
settings_file_exists(file, PREF_KEY_VIBE_SCORE_NOTIFICATIONS,
strlen(PREF_KEY_VIBE_SCORE_NOTIFICATIONS));
if (!user_has_migrated_to_vibe_scores) {
// If the user previously set a vibration intensity, set the vibe scores based on that intensity
if (settings_file_exists(file, PREF_KEY_VIBE_INTENSITY, strlen(PREF_KEY_VIBE_INTENSITY))) {
prv_set_vibe_scores_based_on_legacy_intensity(s_vibe_intensity);
} else if (rtc_is_timezone_set()) {
// Otherwise, if the timezone has been set, then we assume this is a user on 3.10 and lower
// that has not touched their vibe intensity preferences.
// rtc_is_timezone_set() was chosen because it is a setting that gets written when the user
// connects their watch to a phone
prv_set_vibe_scores_based_on_legacy_intensity(DEFAULT_VIBE_INTENSITY);
}
}
// PREF_KEY_VIBE, which used to track whether the user enabled/disabled vibrations, has been
// deprecated in favor of the "disabled vibe score", VibeScoreId_Disabled, so switch to using it
// and delete PREF_KEY_VIBE from the settings file if PREF_KEY_VIBE exists in the settings file
if (settings_file_exists(file, PREF_KEY_VIBE, strlen(PREF_KEY_VIBE))) {
if (!s_vibe_on_notification) {
s_vibe_score_notifications = VibeScoreId_Disabled;
s_vibe_score_incoming_calls = VibeScoreId_Disabled;
}
settings_file_delete(file, PREF_KEY_VIBE, strlen(PREF_KEY_VIBE));
}
}
#endif
void alerts_preferences_init(void) {
s_mutex = mutex_create();
SettingsFile file = {{0}};
if (settings_file_open(&file, FILE_NAME, FILE_LEN) != S_SUCCESS) {
return;
}
#define RESTORE_PREF(key, var) \
do { \
__typeof__(var) _tmp; \
if (settings_file_get( \
&file, key, strlen(key), &_tmp, sizeof(_tmp)) == S_SUCCESS) { \
var = _tmp; \
} \
} while (0)
RESTORE_PREF(PREF_KEY_MASK, s_mask);
RESTORE_PREF(PREF_KEY_VIBE, s_vibe_on_notification);
RESTORE_PREF(PREF_KEY_VIBE_INTENSITY, s_vibe_intensity);
#if CAPABILITY_HAS_VIBE_SCORES
RESTORE_PREF(PREF_KEY_VIBE_SCORE_NOTIFICATIONS, s_vibe_score_notifications);
RESTORE_PREF(PREF_KEY_VIBE_SCORE_INCOMING_CALLS, s_vibe_score_incoming_calls);
RESTORE_PREF(PREF_KEY_VIBE_SCORE_ALARMS, s_vibe_score_alarms);
#endif
RESTORE_PREF(PREF_KEY_DND_MANUALLY_ENABLED, s_do_not_disturb_manually_enabled);
RESTORE_PREF(PREF_KEY_DND_SMART_ENABLED, s_do_not_disturb_smart_dnd_enabled);
RESTORE_PREF(PREF_KEY_DND_INTERRUPTIONS_MASK, s_dnd_interruptions_mask);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE, s_legacy_dnd_schedule);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED, s_legacy_dnd_schedule_enabled);
RESTORE_PREF(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
s_dnd_schedule[WeekdaySchedule].schedule);
RESTORE_PREF(s_dnd_schedule_keys[WeekdaySchedule].enabled_pref_key,
s_dnd_schedule[WeekdaySchedule].enabled);
RESTORE_PREF(s_dnd_schedule_keys[WeekendSchedule].schedule_pref_key,
s_dnd_schedule[WeekendSchedule].schedule);
RESTORE_PREF(s_dnd_schedule_keys[WeekendSchedule].enabled_pref_key,
s_dnd_schedule[WeekendSchedule].enabled);
RESTORE_PREF(PREF_KEY_FIRST_USE_COMPLETE, s_first_use_complete);
RESTORE_PREF(PREF_KEY_NOTIF_WINDOW_TIMEOUT, s_notif_window_timeout_ms);
#undef RESTORE_PREF
prv_migrate_legacy_dnd_schedule(&file);
// tintin watches don't have these prefs, so we can pull this out to save on codespace
#if !PLATFORM_TINTIN
prv_migrate_legacy_first_use_settings(&file);
#endif
#if CAPABILITY_HAS_VIBE_SCORES
prv_migrate_vibe_intensity_to_vibe_scores(&file);
prv_ensure_valid_vibe_scores();
prv_save_all_vibe_scores_to_file(&file);
#endif
settings_file_close(&file);
}
// Convenience macro for setting a string key to a non-pointer value.
#define SET_PREF(key, value) \
prv_set_pref(key, strlen(key), &value, sizeof(value))
static void prv_set_pref(const void *key, size_t key_len, const void *value,
size_t value_len) {
mutex_lock(s_mutex);
SettingsFile file = {{0}};
if (settings_file_open(&file, FILE_NAME, FILE_LEN) != S_SUCCESS) {
goto cleanup;
}
settings_file_set(&file, key, key_len, value, value_len);
settings_file_close(&file);
cleanup:
mutex_unlock(s_mutex);
}
AlertMask alerts_preferences_get_alert_mask(void) {
if (s_mask == AlertMaskAllOnLegacy) {
// Migration for notification settings previously configured under
// old bit setup.
alerts_preferences_set_alert_mask(AlertMaskAllOn);
}
return s_mask;
}
void alerts_preferences_set_alert_mask(AlertMask mask) {
s_mask = mask;
SET_PREF(PREF_KEY_MASK, s_mask);
}
uint32_t alerts_preferences_get_notification_window_timeout_ms(void) {
return s_notif_window_timeout_ms;
}
void alerts_preferences_set_notification_window_timeout_ms(uint32_t timeout_ms) {
s_notif_window_timeout_ms = timeout_ms;
SET_PREF(PREF_KEY_NOTIF_WINDOW_TIMEOUT, s_notif_window_timeout_ms);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//! Vibes
bool alerts_preferences_get_vibrate(void) {
return s_vibe_on_notification;
}
void alerts_preferences_set_vibrate(bool enable) {
s_vibe_on_notification = enable;
SET_PREF(PREF_KEY_VIBE, s_vibe_on_notification);
}
VibeIntensity alerts_preferences_get_vibe_intensity(void) {
return s_vibe_intensity;
}
void alerts_preferences_set_vibe_intensity(VibeIntensity intensity) {
s_vibe_intensity = intensity;
SET_PREF(PREF_KEY_VIBE_INTENSITY, s_vibe_intensity);
}
#if CAPABILITY_HAS_VIBE_SCORES
VibeScoreId alerts_preferences_get_vibe_score_for_client(VibeClient client) {
switch (client) {
case VibeClient_Notifications:
return s_vibe_score_notifications;
case VibeClient_PhoneCalls:
return s_vibe_score_incoming_calls;
case VibeClient_Alarms:
return s_vibe_score_alarms;
default:
WTF;
}
}
void alerts_preferences_set_vibe_score_for_client(VibeClient client, VibeScoreId id) {
const char *key = NULL;
switch (client) {
case VibeClient_Notifications: {
s_vibe_score_notifications = id;
key = PREF_KEY_VIBE_SCORE_NOTIFICATIONS;
break;
}
case VibeClient_PhoneCalls: {
s_vibe_score_incoming_calls = id;
key = PREF_KEY_VIBE_SCORE_INCOMING_CALLS;
break;
}
case VibeClient_Alarms: {
s_vibe_score_alarms = id;
key = PREF_KEY_VIBE_SCORE_ALARMS;
break;
}
default: {
WTF;
}
}
SET_PREF(key, id);
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//! DND
void alerts_preferences_dnd_set_mask(AlertMask mask) {
s_dnd_interruptions_mask = mask;
SET_PREF(PREF_KEY_DND_INTERRUPTIONS_MASK, s_dnd_interruptions_mask);
}
AlertMask alerts_preferences_dnd_get_mask(void) {
return s_dnd_interruptions_mask;
}
bool alerts_preferences_dnd_is_manually_enabled(void) {
return s_do_not_disturb_manually_enabled;
}
void alerts_preferences_dnd_set_manually_enabled(bool enable) {
s_do_not_disturb_manually_enabled = enable;
SET_PREF(PREF_KEY_DND_MANUALLY_ENABLED, s_do_not_disturb_manually_enabled);
}
void alerts_preferences_dnd_get_schedule(DoNotDisturbScheduleType type,
DoNotDisturbSchedule *schedule_out) {
*schedule_out = s_dnd_schedule[type].schedule;
};
void alerts_preferences_dnd_set_schedule(DoNotDisturbScheduleType type,
const DoNotDisturbSchedule *schedule) {
s_dnd_schedule[type].schedule = *schedule;
SET_PREF(s_dnd_schedule_keys[type].schedule_pref_key, s_dnd_schedule[type].schedule);
};
bool alerts_preferences_dnd_is_schedule_enabled(DoNotDisturbScheduleType type) {
return s_dnd_schedule[type].enabled;
}
void alerts_preferences_dnd_set_schedule_enabled(DoNotDisturbScheduleType type, bool on) {
s_dnd_schedule[type].enabled = on;
SET_PREF(s_dnd_schedule_keys[type].enabled_pref_key, s_dnd_schedule[type].enabled);
}
bool alerts_preferences_check_and_set_first_use_complete(FirstUseSource source) {
if (s_first_use_complete & (1 << source)) {
return true;
};
s_first_use_complete |= (1 << source);
SET_PREF(PREF_KEY_FIRST_USE_COMPLETE, s_first_use_complete);
return false;
}
bool alerts_preferences_dnd_is_smart_enabled(void) {
return s_do_not_disturb_smart_dnd_enabled;
}
void alerts_preferences_dnd_set_smart_enabled(bool enable) {
s_do_not_disturb_smart_dnd_enabled = enable;
SET_PREF(PREF_KEY_DND_SMART_ENABLED, s_do_not_disturb_smart_dnd_enabled);
}
void analytics_external_collect_alerts_preferences(void) {
uint8_t alerts_dnd_prefs_bitmask = 0;
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_manually_enabled() << 0);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_smart_enabled() << 1);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_schedule_enabled(WeekdaySchedule) << 2);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_schedule_enabled(WeekendSchedule) << 3);
analytics_set(ANALYTICS_DEVICE_METRIC_ALERTS_DND_PREFS_BITMASK,
alerts_dnd_prefs_bitmask, AnalyticsClient_System);
analytics_set(ANALYTICS_DEVICE_METRIC_ALERTS_MASK,
(uint8_t) alerts_preferences_get_alert_mask(), AnalyticsClient_System);
}