/* * 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 // 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); }