pebble/src/fw/apps/system_apps/settings/settings_notifications.c
2025-01-27 11:38:16 -08:00

390 lines
13 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 "settings_notifications_private.h"
#include "settings_menu.h"
#include "settings_option_menu.h"
#include "settings_window.h"
#include "applib/event_service_client.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/action_menu_window_private.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/option_menu_window.h"
#include "applib/ui/ui.h"
#include "drivers/battery.h"
#include "kernel/pbl_malloc.h"
#include "popups/notifications/notification_window.h"
#include "services/common/analytics/analytics.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/notifications/alerts_preferences_private.h"
#include "services/normal/notifications/alerts_private.h"
#include "services/normal/vibes/vibe_intensity.h"
#include "shell/prefs.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/time/time.h"
#include <stdio.h>
// Offset between vibe intensity menu item index and vibe intensity enum values
#define INTENSITY_ROW_OFFSET 1
typedef struct {
SettingsCallbacks callbacks;
EventServiceInfo battery_connection_event_info;
} SettingsNotificationsData;
enum NotificationsItem {
NotificationsItemFilter,
#if !CAPABILITY_HAS_VIBE_SCORES
NotificationsItemVibration,
#endif
NotificationsItemTextSize,
NotificationsItemWindowTimeout,
NotificationsItem_Count,
};
// Filter Alerts
//////////////////////////
#define NUM_ALERT_MODES_IN_LIST 3
// These aren't all of the values of AlertMask, so to add extra ones you have to update both of
// these arrays
static const AlertMask s_alert_mode_values[NUM_ALERT_MODES_IN_LIST] = {
AlertMaskAllOn,
AlertMaskPhoneCalls,
AlertMaskAllOff,
};
static const char *s_alert_mode_labels[NUM_ALERT_MODES_IN_LIST] = {
i18n_noop("Allow All Notifications"),
i18n_noop("Allow Phone Calls Only"),
i18n_noop("Mute All Notifications"),
};
static const char *prv_alert_mask_to_label(AlertMask mask) {
for (uint32_t i = 0; i < NUM_ALERT_MODES_IN_LIST; i++) {
if (s_alert_mode_values[i] == mask) {
return s_alert_mode_labels[i];
}
}
return "???";
}
static void prv_filter_menu_select(OptionMenu *option_menu, int selection, void *context) {
alerts_set_mask(s_alert_mode_values[selection]);
app_window_stack_remove(&option_menu->window, true /* animated */);
}
static void prv_filter_menu_push(SettingsNotificationsData *data) {
AlertMask mask = alerts_get_mask();
size_t cycle_len = ARRAY_LENGTH(s_alert_mode_values);
size_t index = 0;
// TODO PBL-24306: update once AlertMask logic is made safer
for (size_t i = 0; i < cycle_len; i++) {
if (s_alert_mode_values[i] == mask) {
index = i;
break;
}
}
const OptionMenuCallbacks callbacks = {
.select = prv_filter_menu_select,
};
/// The option in the Settings app for filtering notifications by type.
const char *title = i18n_noop("Filter");
settings_option_menu_push(
title, OptionMenuContentType_DoubleLine, index, &callbacks, cycle_len,
true /* icons_enabled */, s_alert_mode_labels, data);
}
// Vibe Settings (If vibes scores disabled for this model)
//////////////////////////
#if !CAPABILITY_HAS_VIBE_SCORES
static const char *strings_for_vibe_intensities[] = {
i18n_ctx_noop("NotifVibe", "Disabled"),
i18n_ctx_noop("NotifVibe", "Low"),
i18n_ctx_noop("NotifVibe", "Medium"),
i18n_ctx_noop("NotifVibe", "High")
};
static void prv_vibe_menu_select(OptionMenu *option_menu, int selection, void *context) {
const bool enable_vibration = (selection != 0);
const VibeIntensity new_vibe_intensity = enable_vibration ? (selection - INTENSITY_ROW_OFFSET) :
DEFAULT_VIBE_INTENSITY;
alerts_set_vibrate(enable_vibration);
alerts_preferences_set_vibe_intensity(new_vibe_intensity);
vibe_intensity_set(new_vibe_intensity);
if (enable_vibration) {
vibes_short_pulse();
}
app_window_stack_remove(&option_menu->window, true /* animated */);
}
static void prv_vibe_menu_push(SettingsNotificationsData *data) {
const OptionMenuCallbacks callbacks = {
.select = prv_vibe_menu_select,
};
/// The option in the Settings app for choosing a vibration intensity for notifications.
const char *title = i18n_noop("Vibration");
uint32_t selected = vibe_intensity_get() + INTENSITY_ROW_OFFSET;
if (!alerts_get_vibrate()) {
selected = 0;
}
settings_option_menu_push(
title, OptionMenuContentType_SingleLine, selected, &callbacks,
ARRAY_LENGTH(strings_for_vibe_intensities), true /* icons_enabled */,
strings_for_vibe_intensities, data);
}
#endif /* !CAPABILITY_HAS_VIBE_SCORES */
// Text Size
////////////////////////
static const char *s_text_size_names[] = {
[SettingsContentSize_Small] = i18n_noop("Smaller"),
[SettingsContentSize_Default] = i18n_noop("Default"),
[SettingsContentSize_Large] = i18n_noop("Larger"),
};
static void prv_text_size_menu_select(OptionMenu *option_menu, int selection, void *context) {
system_theme_set_content_size(settings_content_size_to_preferred_size(selection));
app_window_stack_remove(&option_menu->window, true /* animated */);
}
static void prv_text_size_menu_push(SettingsNotificationsData *data) {
const OptionMenuCallbacks callbacks = {
.select = prv_text_size_menu_select,
};
/// The option in the Settings app for choosing the text size of notifications.
const char *title = i18n_noop("Text Size");
const SettingsContentSize index =
settings_content_size_from_preferred_size(system_theme_get_content_size());
settings_option_menu_push(
title, OptionMenuContentType_SingleLine, index, &callbacks, SettingsContentSizeCount,
true /* icons_enabled */, s_text_size_names, data);
}
// Text Size
////////////////////////
// NOTE: Keep the following two arrays in sync and with the same size.
static const uint32_t s_window_timeouts_ms[] = {
15 * MS_PER_SECOND,
1 * MS_PER_MINUTE,
NOTIF_WINDOW_TIMEOUT_DEFAULT,
10 * MS_PER_MINUTE,
NOTIF_WINDOW_TIMEOUT_INFINITE
};
static const char *s_window_timeouts_labels[] = {
/// 15 Second Notification Window Timeout
i18n_noop("15 Seconds"),
/// 1 Minute Notification Window Timeout
i18n_noop("1 Minute"),
/// 3 Minute Notification Window Timeout
i18n_noop("3 Minutes"),
/// 10 Minute Notification Window Timeout
i18n_noop("10 Minutes"),
/// No Notification Window Timeout
i18n_noop("None"),
};
_Static_assert(ARRAY_LENGTH(s_window_timeouts_ms) == ARRAY_LENGTH(s_window_timeouts_labels), "");
static int prv_window_timeout_get_selection_index(void) {
const int DEFAULT_IDX = 2;
// Double check no one has fudged with the order and the fallback/default
PBL_ASSERTN(s_window_timeouts_ms[DEFAULT_IDX] == NOTIF_WINDOW_TIMEOUT_DEFAULT);
const uint32_t timeout_ms = alerts_preferences_get_notification_window_timeout_ms();
for (size_t i = 0; i < ARRAY_LENGTH(s_window_timeouts_ms); i++) {
if (s_window_timeouts_ms[i] == timeout_ms) {
return i;
}
}
// Should never happen (only should happen if we remove a timeout and don't migrate the user
// to a new setting automatically
return DEFAULT_IDX;
}
static void prv_window_timeout_menu_select(OptionMenu *option_menu, int selection, void *context) {
alerts_preferences_set_notification_window_timeout_ms(s_window_timeouts_ms[selection]);
app_window_stack_remove(&option_menu->window, true /* animated */);
}
static void prv_window_timeout_menu_push(SettingsNotificationsData *data) {
const int index = prv_window_timeout_get_selection_index();
const OptionMenuCallbacks callbacks = {
.select = prv_window_timeout_menu_select,
};
/// Status bar title for the Notification Window Timeout settings screen
const char *title = i18n_noop("Timeout");
settings_option_menu_push(
title, OptionMenuContentType_SingleLine, index, &callbacks,
ARRAY_LENGTH(s_window_timeouts_labels), true /* icons_enabled */, s_window_timeouts_labels,
data);
}
// Menu Layer Callbacks
////////////////////////
static uint16_t prv_num_rows_cb(SettingsCallbacks *context) {
return NotificationsItem_Count;
}
static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx,
const Layer *cell_layer, uint16_t row, bool selected) {
SettingsNotificationsData *data = ((SettingsOptionMenuData *)context)->context;
const char *subtitle = NULL;
const char *title = NULL;
switch (row) {
case NotificationsItemFilter:
title = i18n_noop("Filter");
subtitle = prv_alert_mask_to_label(alerts_get_mask());
break;
#if !CAPABILITY_HAS_VIBE_SCORES
case NotificationsItemVibration:
title = i18n_noop("Vibration");
if (battery_is_usb_connected()) {
subtitle = i18n_noop("Disabled (Plugged In)");
} else if (alerts_get_vibrate()) {
subtitle = strings_for_vibe_intensities[vibe_intensity_get() + INTENSITY_ROW_OFFSET];
} else {
subtitle = strings_for_vibe_intensities[0];
}
break;
#endif /* !CAPABILITY_HAS_VIBE_SCORES */
case NotificationsItemTextSize: {
/// String within Settings->Notifications that describes the text font size
title = i18n_noop("Text Size");
const SettingsContentSize index =
settings_content_size_from_preferred_size(system_theme_get_content_size());
subtitle = (index < SettingsContentSizeCount) ? s_text_size_names[index] : "";
break;
}
case NotificationsItemWindowTimeout: {
/// String within Settings->Notifications that describes the window timeout setting
title = i18n_noop("Timeout");
subtitle = s_window_timeouts_labels[prv_window_timeout_get_selection_index()];
break;
}
default:
WTF;
}
menu_cell_basic_draw(ctx, cell_layer, i18n_get(title, data), i18n_get(subtitle, data), NULL);
}
static void prv_deinit_cb(SettingsCallbacks *context) {
SettingsNotificationsData *data = (SettingsNotificationsData *)context;
i18n_free_all(data);
app_free(data);
}
static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) {
SettingsNotificationsData *data = (SettingsNotificationsData *) context;
switch (row) {
case NotificationsItemFilter:
prv_filter_menu_push(data);
break;
#if !CAPABILITY_HAS_VIBE_SCORES
case NotificationsItemVibration:
if (battery_is_usb_connected()) {
return;
}
prv_vibe_menu_push(data);
break;
#endif /* !CAPABILITY_HAS_VIBE_SCORES */
case NotificationsItemTextSize:
prv_text_size_menu_push(data);
break;
case NotificationsItemWindowTimeout:
prv_window_timeout_menu_push(data);
break;
default:
WTF;
}
settings_menu_reload_data(SettingsMenuItemNotifications);
}
static void prv_settings_notifications_event_handler(PebbleEvent *event, void *context) {
switch (event->type) {
case PEBBLE_BATTERY_CONNECTION_EVENT:
// Redraw the menu so that the Vibration status will be re-rendered.
settings_menu_mark_dirty(SettingsMenuItemNotifications);
break;
default:
break;
}
}
static void prv_expand_cb(SettingsCallbacks *context) {
SettingsNotificationsData *data = (SettingsNotificationsData *) context;
data->battery_connection_event_info = (EventServiceInfo) {
.type = PEBBLE_BATTERY_CONNECTION_EVENT,
.handler = prv_settings_notifications_event_handler,
};
event_service_client_subscribe(&data->battery_connection_event_info);
}
static void prv_hide_cb(SettingsCallbacks *context) {
SettingsNotificationsData *data = (SettingsNotificationsData *) context;
event_service_client_unsubscribe(&data->battery_connection_event_info);
}
static Window *prv_init(void) {
SettingsNotificationsData* data = app_malloc_check(sizeof(*data));
*data = (SettingsNotificationsData){};
data->callbacks = (SettingsCallbacks) {
.deinit = prv_deinit_cb,
.draw_row = prv_draw_row_cb,
.select_click = prv_select_click_cb,
.num_rows = prv_num_rows_cb,
.expand = prv_expand_cb,
.hide = prv_hide_cb,
};
return settings_window_create(SettingsMenuItemNotifications, &data->callbacks);
}
const SettingsModuleMetadata *settings_notifications_get_info(void) {
static const SettingsModuleMetadata s_module_info = {
.name = i18n_noop("Notifications"),
.init = prv_init,
};
return &s_module_info;
}
void analytics_external_collect_notification_settings(void) {
const uint8_t strength = get_strength_for_intensity(vibe_intensity_get());
analytics_set(ANALYTICS_DEVICE_METRIC_SETTING_VIBRATION_STRENGTH,
strength, AnalyticsClient_System);
}