Files
pebble/src/fw/popups/notifications/notification_window.c
Josh Soref cfea0fc3c2 spelling: reload
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-29 00:03:27 -05:00

1499 lines
56 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 "notification_window.h"
#include "notifications_presented_list.h"
#include "notification_window_private.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/action_button.h"
#include "applib/ui/action_menu_window.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/ui.h"
#include "applib/ui/window.h"
#include "applib/ui/window_manager.h"
#include "applib/ui/window_stack.h"
#include "apps/system_apps/timeline/peek_layer.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "os/mutex.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/analytics/analytics.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "services/common/comm_session/session.h"
#include "services/common/evented_timer.h"
#include "services/common/i18n/i18n.h"
#include "services/common/light.h"
#include "services/common/regular_timer.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
#include "services/normal/blob_db/pin_db.h"
#include "services/normal/blob_db/reminder_db.h"
#include "services/normal/notifications/alerts.h"
#include "services/normal/notifications/alerts_preferences.h"
#include "services/normal/notifications/alerts_private.h"
#include "services/normal/notifications/ancs/ancs_filtering.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/notifications/notification_storage.h"
#include "services/normal/notifications/notification_types.h"
#include "services/normal/notifications/notifications.h"
#include "services/normal/timeline/attribute.h"
#include "services/normal/timeline/notification_layout.h"
#include "services/normal/timeline/swap_layer.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/timeline/timeline_actions.h"
#include "services/normal/timeline/timeline_resources.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/trig.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#if CAPABILITY_HAS_VIBE_SCORES
#include "services/normal/vibes/vibe_client.h"
#include "services/normal/vibes/vibe_score.h"
#endif
#define NOTIFICATION_PRIORITY (ModalPriorityNotification)
#define NUM_MOOOK_SOFT_MID_FRAMES PBL_IF_RECT_ELSE(6, 4)
#define FIRST_PEEK_DELAY PBL_IF_RECT_ELSE(500, 200)
// pop timer for window. Refreshed during any point of activity (button clicks)
static const unsigned int QUICK_DND_HOLD_MS = 800;
T_STATIC NotificationWindowData s_notification_window_data;
static bool s_in_use = false;
PebbleMutex *s_notification_window_mutex;
static bool prv_should_provide_action_menu_for_item(NotificationWindowData *data,
const TimelineItem *item);
static void prv_handle_notification_removed_common(Uuid *, NotificationType);
static bool prv_should_pop_due_to_inactivity(void);
/////////////////////
// Helpers
/////////////////////
static AlertType prv_alert_type_for_notification_type(NotificationType type) {
switch (type) {
case NotificationMobile:
return AlertMobile;
case NotificationPhoneCall:
return AlertPhoneCall;
case NotificationOther:
return AlertOther;
case NotificationReminder:
return AlertReminder;
default:
return AlertInvalid;
}
}
static void prv_toggle_dnd_from_back_click(ClickRecognizerRef recognizer, void *data) {
do_not_disturb_manual_toggle_with_dialog();
}
static void prv_toggle_dnd_from_action_menu(ActionMenu *action_menu,
const ActionMenuItem *item,
void *context) {
// This function handles first-time use tutorial logic
do_not_disturb_toggle_manually_enabled(ManualDNDFirstUseSourceActionMenu);
}
static TimelineItem *prv_get_current_notification(NotificationWindowData *data) {
if (!notifications_presented_list_current()) {
return NULL;
}
LayoutLayer *current = swap_layer_get_current_layout(&data->swap_layer);
TimelineItem *item = (TimelineItem *)layout_get_context(current);
return item;
}
static void prv_draw_dnd_icon(struct Layer *layer, GContext *ctx) {
if (!s_in_use) {
return;
}
NotificationWindowData *data = &s_notification_window_data;
if (!data->dnd_icon_visible) {
return;
}
graphics_context_set_tint_color(ctx, status_bar_layer_get_foreground_color(&data->status_layer));
graphics_context_set_compositing_mode(ctx, GCompOpTint);
graphics_draw_bitmap_in_rect(ctx, &data->dnd_icon, &data->dnd_icon.bounds);
}
static void prv_update_status_layer(NotificationWindowData *data) {
const int notif_count = notifications_presented_list_count();
if (notif_count <= 1) {
// if less than one notification, clear the status bar info
status_bar_layer_reset_info(&data->status_layer);
} else {
// if more than one, then show the current index in relation to the total number
status_bar_layer_set_info_progress(&data->status_layer,
notifications_presented_list_current_idx() + 1,
notif_count);
}
}
static void prv_cleanup_timer(EventedTimerID *timer_id) {
if (*timer_id != EVENTED_TIMER_INVALID_ID) {
evented_timer_cancel(*timer_id);
*timer_id = EVENTED_TIMER_INVALID_ID;
}
}
static void prv_cancel_reminder_watchdog(NotificationWindowData *data) {
if (regular_timer_is_scheduled(&data->reminder_watchdog_timer_id)) {
regular_timer_remove_callback(&data->reminder_watchdog_timer_id);
}
}
static void prv_cleanup_timers(NotificationWindowData *data) {
prv_cleanup_timer(&data->pop_timer_id);
prv_cancel_reminder_watchdog(data);
prv_cleanup_timer(&data->peek_layer_timer);
}
static void prv_pop_notification_window(NotificationWindowData *data) {
if (data->window_frozen || !s_in_use) {
return;
}
// This calls through to our window_unload() callback, which cancels our timer and clears s_in_use
window_stack_remove(&data->window, true /* animated */);
}
static int prv_reminders_on_top_comparator(void *a, void *b) {
NotifList *notif_a = (NotifList*) a;
NotifList *notif_b = (NotifList*) b;
NotificationType type_a = notif_a->notif.type;
NotificationType type_b = notif_b->notif.type;
// Reminders come first, then everything else. More recent reminders should appear before older
// reminders and more recent notifications should appear before older notifications
if (type_b == NotificationReminder) {
return 1;
} else if (type_b != NotificationReminder && type_a != NotificationReminder) {
return 1;
} else {
return -1;
}
}
static void prv_notification_window_add_notification(Uuid *id, NotificationType type) {
if (do_not_disturb_is_active()) {
notifications_presented_list_add_sorted(id, type, prv_reminders_on_top_comparator, false);
} else {
notifications_presented_list_add(id, type);
}
}
static void prv_reload_swap_layer(NotificationWindowData *data) {
// If the action menu is on the screen, then don't reload the swap layer.
// The action menu's context is just a pointer to the swap layer's layout layer's context.
// Reloading the swap layer will give the action menu a bogus timeline item pointer.
// Also if the action menu is up, we don't need to reload the swap layer until the
// notification window appears again
if (data->action_menu && window_is_loaded((Window *)data->action_menu)) {
data->notifications_modified = true;
} else {
swap_layer_reload_data(&data->swap_layer);
}
}
/////////////////////
// Dismiss All
/////////////////////
static void prv_handle_dismiss_all_complete(bool succeeded, void *cb_data) {
NotificationWindowData *window_data = (NotificationWindowData*) cb_data;
window_data->window_frozen = false;
if (s_in_use && succeeded) {
prv_pop_notification_window(window_data);
}
}
static void prv_dismiss_all(void *data, ActionMenu *action_menu) {
NotificationWindowData *window_data = (NotificationWindowData*) data;
const int num_notifications = notifications_presented_list_count();
if (num_notifications == 0) {
return;
}
Uuid *first_id = notifications_presented_list_first();
NotificationInfo *notif_list = kernel_malloc_check(sizeof(NotificationInfo) * num_notifications);
for (int i = 0; i < num_notifications; i++) {
Uuid *id = notifications_presented_list_relative(first_id, i);
memcpy(&notif_list[i].id, id, sizeof(Uuid));
notif_list[i].type = notifications_presented_list_get_type(id);
}
PBL_LOG(LOG_LEVEL_DEBUG, "Dismissing %d notifications", num_notifications);
window_data->window_frozen = true;
timeline_actions_dismiss_all(notif_list,
num_notifications,
action_menu,
prv_handle_dismiss_all_complete,
data);
kernel_free(notif_list);
}
static void prv_dismiss_all_action_cb(ActionMenu *action_menu,
const ActionMenuItem *item,
void *context) {
NotificationWindowData *window_data = (NotificationWindowData*) item->action_data;
prv_dismiss_all(window_data, action_menu);
}
static int64_t prv_interpolate_moook_peek_animation(int32_t normalized, int64_t from, int64_t to) {
return interpolate_moook_soft(normalized, from, to, NUM_MOOOK_SOFT_MID_FRAMES);
}
// scroll = true, then scroll the layer up dy
// scroll = false, then shrink the layer's size by dy
static Animation *prv_create_anim_frame(Layer *layer, int16_t dy, bool scroll) {
GRect *start = &layer->frame;
GRect stop = *start;
if (scroll) {
stop.origin.y += dy;
} else {
stop.size.h += dy;
}
PropertyAnimation *prop_anim = property_animation_create_layer_frame(layer, start, &stop);
Animation *animation = property_animation_get_animation(prop_anim);
animation_set_duration(animation, interpolate_moook_soft_duration(NUM_MOOOK_SOFT_MID_FRAMES));
animation_set_custom_interpolation(animation, prv_interpolate_moook_peek_animation);
return animation;
}
/////////////////////
// Peek Layer
/////////////////////
static void prv_peek_anim_stopped(Animation *animation, bool finished, void *context) {
NotificationWindowData *data = context;
data->first_notif_loaded = true;
peek_layer_destroy(data->peek_layer);
data->peek_layer = NULL;
TimelineItem *item = prv_get_current_notification(data);
layer_set_hidden((Layer *)&data->action_button_layer,
!prv_should_provide_action_menu_for_item(data, item));
}
static void prv_hide_peek_layer(void *context) {
NotificationWindowData *data = context;
// get the frame of the swap_layer and set its destination
const GRect *swap_frame = &data->swap_layer.layer.frame;
int16_t swap_frame_animation_dy = STATUS_BAR_LAYER_HEIGHT - swap_frame->origin.y;
// duration of animation of both peek layer and swap layer moving up to the top
int16_t peek_frame_animation_dy = swap_frame_animation_dy;
#if PBL_ROUND
// Needed because the peek layer's background and the screen have different sizes,
// so the peek layer needs to move a different number of pixels vs the swap layer
const int16_t peek_circle_vertical_offset = (BANNER_CIRCLE_RADIUS - (DISP_ROWS / 2)) / 2;
peek_frame_animation_dy -= peek_circle_vertical_offset;
#endif
Animation *peek_up = prv_create_anim_frame((Layer *)data->peek_layer, peek_frame_animation_dy,
false /* scroll */);
Animation *swap_up = prv_create_anim_frame((Layer *)&data->swap_layer, swap_frame_animation_dy,
true /* scroll */);
Animation *spawn = animation_spawn_create(peek_up, swap_up, NULL);
AnimationHandlers anim_handlers = {
.started = NULL,
.stopped = prv_peek_anim_stopped,
};
animation_set_handlers(spawn, anim_handlers, data);
// move the icon to where it should be in the swap_layer's notification_layout
PeekLayer *peek_layer = data->peek_layer;
GRect frame = peek_layer->layer.frame;
frame.origin.x = (frame.size.w / 2) - (NOTIFICATION_TINY_RESOURCE_SIZE.w / 2);
frame.origin.y = CARD_ICON_UPPER_PADDING + STATUS_BAR_LAYER_HEIGHT;
frame.size = NOTIFICATION_TINY_RESOURCE_SIZE;
const bool align_in_frame = true;
peek_layer_set_scale_to_image(peek_layer, &data->peek_icon_info, TimelineResourceSizeTiny, frame,
align_in_frame);
// set peek_layer clips to true so I can resize the peek_layer's background
layer_set_clips(&peek_layer->layer, true);
peek_layer_play(peek_layer);
data->peek_animation = spawn;
animation_schedule(spawn);
}
static void prv_play_peek_layer(void *context) {
NotificationWindowData *data = context;
// play the peek layer unfold sequence
peek_layer_play(data->peek_layer);
const uint16_t PEEK_LAYER_HIDE_DELAY = PBL_IF_RECT_ELSE(500, 400);
data->peek_layer_timer = evented_timer_register_or_reschedule(data->peek_layer_timer,
PEEK_LAYER_HIDE_DELAY,
prv_hide_peek_layer,
data);
}
static void prv_show_peek_for_notification(NotificationWindowData *data, Uuid *id,
bool is_first_notification) {
// reload everything, doesn't matter since it will be covered by the peek layer
notification_window_focus_notification(id, false);
// if the peek animation is already in progress, we've done all we need
// data->peek_layer is only ever not null between the start and end of the peek
// animation; it's cleaned up by prv_peek_anim_stopped, and initialized here
if (data->peek_layer) {
return;
}
// get root layer of window and make the peek layer the full size
const GRect *peek_layer_frame = &window_get_root_layer(&data->window)->frame;
data->peek_layer = peek_layer_create(*peek_layer_frame);
if (!data->peek_layer) {
if (is_first_notification) {
// we don't have enough memory, no peek. Just push the modal window.
modal_window_push(&data->window, NOTIFICATION_PRIORITY, true /* animated */);
}
return;
}
// get the current layout so we can get the color and icon
LayoutLayer *layout = swap_layer_get_current_layout(&data->swap_layer);
if (!layout) {
return;
}
// Get color and icon
const LayoutColors *colors = layout_get_notification_colors(layout);
TimelineItem *item = prv_get_current_notification(data);
TimelineResourceId timeline_res_id;
const TimelineResourceId fallback_icon_id = notification_layout_get_fallback_icon_id(
item->header.type);
timeline_res_id = attribute_get_uint32(&item->attr_list, AttributeIdIconTiny,
fallback_icon_id);
data->peek_icon_info = (TimelineResourceInfo) {
.res_id = timeline_res_id,
.app_id = &data->notification_app_id, // This is set earlier when we reload the layout
.fallback_id = fallback_icon_id,
};
peek_layer_set_icon(data->peek_layer, &data->peek_icon_info);
peek_layer_set_background_color(data->peek_layer, colors->bg_color);
// This is so that only the banner of the swap_layer is sticking out from the bottom
GRect swap_frame = ((Layer *)&data->swap_layer)->frame;
swap_frame.origin.y = swap_frame.origin.y + swap_frame.size.h -
PBL_IF_RECT_ELSE(LAYOUT_BANNER_HEIGHT_RECT, LAYOUT_TOP_BANNER_HEIGHT_ROUND);
layer_set_frame((Layer *)&data->swap_layer, &swap_frame);
// play the peek layer after the delay, more delay for the first notification
// because we're coming from the compositor modal transition
const uint16_t peek_layer_play_delay = is_first_notification ? FIRST_PEEK_DELAY : 100;
data->peek_layer_timer = evented_timer_register_or_reschedule(data->peek_layer_timer,
peek_layer_play_delay,
prv_play_peek_layer,
data);
// insert below status bar but above everything else.
Window *window = &data->window;
layer_add_child(window_get_root_layer(window), (Layer *)data->peek_layer);
layer_insert_below_sibling((Layer *)data->peek_layer, (Layer *)&data->status_layer);
}
///////////////////////
// SwapLayer Callbacks
///////////////////////
static void prv_remove_notification(NotificationWindowData *data, Uuid *notif_id,
const bool should_close_am) {
// We have to check if the current presented notification is the one being
// viewed. If it is, then we check if we have an action menu and close it.
// If we have an action menu and it's frozen we are waiting on an action result
// from that menu; this will cleanup the action menu on completion anyway so don't do it here
if (should_close_am && uuid_equal(notifications_presented_list_current(), notif_id) &&
data->action_menu && ((Window *)data->action_menu)->on_screen &&
!action_menu_is_frozen(data->action_menu)) {
action_menu_close(data->action_menu, true);
}
// Setting the next ID is handled by the service
notifications_presented_list_remove(notif_id);
if ((notifications_presented_list_current_idx() < 0) && s_in_use) {
prv_pop_notification_window(data);
return;
}
prv_reload_swap_layer(data);
}
static void prv_layout_removed_handler(SwapLayer *swap_layer, LayoutLayer *layout, void *context) {
TimelineItem *item = layout_get_context(layout);
timeline_item_destroy(item);
layout_destroy(layout);
}
T_STATIC LayoutLayer *prv_get_layout_handler(SwapLayer *swap_layer, int8_t rel_position,
void *context) {
NotificationWindowData *data = context;
Uuid *id = notifications_presented_list_relative(
notifications_presented_list_current(), rel_position);
// if no layers, don't return one
if (uuid_is_invalid(id)) {
return NULL;
}
NotificationType type = notifications_presented_list_get_type(id);
TimelineItem *item = task_zalloc_check(sizeof(TimelineItem));
if (type == NotificationMobile) {
if (!notification_storage_get(id, item)) {
PBL_LOG(LOG_LEVEL_ERROR, "Failed to read notification");
goto cleanup;
}
} else if (type == NotificationReminder) {
// validate reminder
int rv = reminder_db_read_item(item, id);
if (rv != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Failed to read reminder");
goto cleanup;
}
}
// Determine if the icon isn't a system resource (meaning we have to load its associated app id)
TimelineResourceId icon = attribute_get_uint32(&item->attr_list, AttributeIdIconTiny,
TIMELINE_RESOURCE_INVALID);
TimelineItem pin;
if (timeline_resources_is_system(icon) ||
pin_db_read_item_header(&pin, &item->header.parent_id) != S_SUCCESS) {
data->notification_app_id = (Uuid)UUID_INVALID;
} else {
data->notification_app_id = pin.header.parent_id;
}
const LayoutId layout_id = (type == NotificationMobile) ? LayoutIdNotification : LayoutIdReminder;
NotificationLayoutInfo layout_info = (NotificationLayoutInfo) {
.item = item,
.show_notification_timestamp = !prv_should_pop_due_to_inactivity()
};
const LayoutLayerConfig config = {
.frame = &data->window.layer.bounds,
.attributes = &item->attr_list,
.mode = LayoutLayerModeCard,
.app_id = &data->notification_app_id,
.context = &layout_info
};
NotificationLayout *notification_layout = (NotificationLayout *)layout_create(layout_id, &config);
return &notification_layout->layout;
cleanup:
timeline_item_destroy(item);
return NULL;
}
//////////////////////
// Timer Functions
//////////////////////
static bool prv_should_pop_due_to_inactivity(void) {
// If not a modal, then we are in the notification history app and the pop timer makes no sense
// If in DND mode we want to keep the notifications on the screen
return s_in_use && s_notification_window_data.is_modal && !do_not_disturb_is_active();
}
static void prv_pop_timer_callback(void *data) {
NotificationWindowData *window_data = data;
window_data->pop_timer_id = EVENTED_TIMER_INVALID_ID;
// It's possible that our timeout expired at the same time the window was dismissed
// through a button press or something like that. So, ignore this CALLBACK event posted
// by our timer if our window is already down (s_in_use false)
if (s_in_use) {
prv_pop_notification_window(window_data);
}
}
static void prv_refresh_pop_timer_with_timeout(NotificationWindowData *data, uint32_t timeout,
bool final) {
if (!prv_should_pop_due_to_inactivity()) {
return;
}
if (data->action_menu == NULL) {
// If the user has an action menu open, then we don't want to refresh the pop timeout,
// as they are still interacting with the Notification stack
data->pop_timer_is_final = final;
data->pop_timer_id = evented_timer_register_or_reschedule(data->pop_timer_id, timeout,
prv_pop_timer_callback, data);
}
}
static void prv_refresh_pop_timer(NotificationWindowData *data) {
if (data->pop_timer_is_final) {
return;
}
const uint32_t timeout_ms = alerts_get_notification_window_timeout_ms();
prv_refresh_pop_timer_with_timeout(data, timeout_ms, false);
}
static void prv_pop_notification_window_after_delay(NotificationWindowData *data,
uint32_t delay_ms) {
prv_refresh_pop_timer_with_timeout(data, delay_ms, true);
}
static time_t prv_get_stale_time(TimelineItem *item) {
// Reminders become stale 10 minutes after their start time, or when the event is over
return item->header.timestamp + MAX(10, item->header.duration) * SECONDS_PER_MINUTE;
}
static void prv_clear_if_stale_reminder(Uuid *id, NotificationType type, void *cb_data) {
NotificationWindowData *window_data = cb_data;
if (type != NotificationReminder) {
return;
}
TimelineItem reminder;
if (S_SUCCESS != reminder_db_read_item(&reminder, id)) {
return;
}
timeline_item_free_allocated_buffer(&reminder);
TimelineItem item;
if (S_SUCCESS != pin_db_get(&reminder.header.parent_id, &item)) {
return;
}
timeline_item_free_allocated_buffer(&item);
// Use the latest stale time to auto-hide the reminder.
const time_t reminder_stale_time = prv_get_stale_time(&reminder);
const time_t event_stale_time = prv_get_stale_time(&item);
const time_t stale_time = MAX(reminder_stale_time, event_stale_time);
const time_t now = rtc_get_time();
if (stale_time <= now && window_data->is_modal) {
PBL_LOG(LOG_LEVEL_INFO, "Removing stale reminder from notification popup window");
prv_remove_notification(window_data, id, true /* close am */);
}
}
static void prv_clear_stale_reminders(void *data) {
notifications_presented_list_each(prv_clear_if_stale_reminder, data);
}
static void prv_clear_stale_reminders_timer_cb(void *data) {
// This functionality only exists for popups (modal windows) which currently all
// run on Kernel Main
launcher_task_add_callback(prv_clear_stale_reminders, data);
}
static void prv_setup_reminder_watchdog(NotificationWindowData *data) {
if (!data->is_modal || regular_timer_is_scheduled(&data->reminder_watchdog_timer_id) ||
!do_not_disturb_is_active()) {
return;
}
data->reminder_watchdog_timer_id = (const RegularTimerInfo) {
.cb = prv_clear_stale_reminders_timer_cb,
.cb_data = data,
};
regular_timer_add_minutes_callback(&data->reminder_watchdog_timer_id);
}
///////////////////////
// Clicks
///////////////////////
static bool prv_should_show_action_in_action_menu(NotificationWindowData *data,
const TimelineItem *item,
const TimelineItemAction *action) {
if (timeline_item_is_ancs_notif(item)) {
if (data->is_modal) {
// If we are in the modal popup show all available actions. We are fairly certain that the
// notification will still be in the notification center at this point so all ANCS actions
// should work
return true;
} else {
// If we are in the notifications app, only show non ANCS actions. Pre iOS9 we can't really
// know if the notification is still in the notification center or not, so we play it safe
// and only show non ACNS actions. Once iOS9 is more widespread we can look at updating this
return !timeline_item_action_is_ancs(action);
}
} else { // Android
// Show all actions unless the item has already been acted upon, in which case show none
return (!item->header.actioned &&
!item->header.dismissed &&
(data->is_modal ||
comm_session_has_capability(comm_session_get_system_session(),
CommSessionExtendedNotificationService)));
}
}
static bool prv_should_provide_action_menu_for_item(NotificationWindowData *data,
const TimelineItem *item) {
for (int i = 0; i < item->action_group.num_actions; i++) {
TimelineItemAction *action = &item->action_group.actions[i];
if (prv_should_show_action_in_action_menu(data, item, action)) {
return true;
}
}
return false;
}
static WindowStack *prv_get_window_stack(void) {
if (pebble_task_get_current() == PebbleTask_App) {
return app_state_get_window_stack();
}
return modal_manager_get_window_stack(ModalPriorityNotification);
}
static void prv_push_snooze_dialog(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Snooze");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *msg = i18n_get("Snoozed", dialog);
dialog_set_text(dialog, msg);
dialog_set_icon(dialog, RESOURCE_ID_REMINDER_SNOOZE);
i18n_free(msg, dialog);
dialog_set_text_color(dialog, GColorWhite);
dialog_set_fullscreen(dialog, true);
dialog_set_background_color(dialog, GColorBlueMoon);
dialog_set_timeout(dialog, 1700);
simple_dialog_push(simple_dialog, prv_get_window_stack());
}
static void prv_snooze_reminder_cb(ActionMenu *action_menu,
const ActionMenuItem *action_menu_item,
void *context) {
NotificationWindowData *window_data = (NotificationWindowData *) action_menu_item->action_data;
TimelineItem *item = prv_get_current_notification(window_data);
// Snooze reminder.
// It's highly unlikely we'll get E_INVALID_OPERATION based on the snooze logic parameters.
if (reminders_snooze((Reminder *) item) == S_SUCCESS) {
prv_push_snooze_dialog();
}
// Dismiss reminder
const TimelineItemAction *action = timeline_item_find_dismiss_action(item);
if (action) {
timeline_invoke_action(item, action, NULL);
}
}
static void prv_push_muted_dialog(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Muted");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *msg = i18n_get("Muted", dialog);
dialog_set_text(dialog, msg);
dialog_set_icon(dialog, RESOURCE_ID_RESULT_MUTE_LARGE);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DEFAULT);
i18n_free(msg, dialog);
simple_dialog_push(simple_dialog, prv_get_window_stack());
}
static void prv_mute_notification(const ActionMenuItem *action_menu_item,
uint8_t muted_bitfield) {
NotificationWindowData *window_data = action_menu_item->action_data;
TimelineItem *item = prv_get_current_notification(window_data);
const char *app_id = attribute_get_string(&item->attr_list, AttributeIdiOSAppIdentifier, "");
if (!*app_id) {
PBL_LOG(LOG_LEVEL_ERROR, "Could not mute notification. Unknown app_id");
return;
}
const int app_id_len = strlen(app_id);
iOSNotifPrefs *notif_prefs = ios_notif_pref_db_get_prefs((uint8_t *)app_id, app_id_len);
if (notif_prefs && attribute_find(&notif_prefs->attr_list, AttributeIdMuteDayOfWeek)) {
attribute_list_add_uint8(&notif_prefs->attr_list, AttributeIdMuteDayOfWeek, muted_bitfield);
ios_notif_pref_db_store_prefs((uint8_t *)app_id, app_id_len,
&notif_prefs->attr_list, &notif_prefs->action_group);
TimelineItemAction *dismiss = timeline_item_find_dismiss_action(item);
if (dismiss) {
timeline_invoke_action(item, dismiss, NULL);
}
prv_push_muted_dialog();
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_MUTED_COUNT, AnalyticsClient_System);
} else {
// This is a very unlikely case. We store some default prefs which includes the mute
// attribute when we receive the notification so either someone deleted the entry
// in the DB or the mute attribute (neither of which should happen)
PBL_LOG(LOG_LEVEL_WARNING, "Could not mute notification. No prefs or mute attribute");
}
ios_notif_pref_db_free_prefs(notif_prefs);
}
static void prv_mute_notification_always(ActionMenu *action_menu,
const ActionMenuItem *action_menu_item,
void *context) {
prv_mute_notification(action_menu_item, MuteBitfield_Always);
}
static void prv_mute_notification_weekdays(ActionMenu *action_menu,
const ActionMenuItem *action_menu_item,
void *context) {
prv_mute_notification(action_menu_item, MuteBitfield_Weekdays);
}
static void prv_mute_notification_weekends(ActionMenu *action_menu,
const ActionMenuItem *action_menu_item,
void *context) {
prv_mute_notification(action_menu_item, MuteBitfield_Weekends);
}
static bool prv_has_mute_action(TimelineItem *item) {
PebbleProtocolCapabilities capabilities;
bt_persistent_storage_get_cached_system_capabilities(&capabilities);
return timeline_item_is_ancs_notif(item) && capabilities.notification_filtering_support;
}
static ActionMenuLevel *prv_create_action_menu_for_item(TimelineItem *item,
NotificationWindowData *window_data,
TimelineItemActionSource source) {
// Determine action menu properties
int num_timeline_actions = 0;
TimelineItemAction *dismiss_action = NULL;
for (int i = 0; i < item->action_group.num_actions; i++) {
TimelineItemAction *action = &item->action_group.actions[i];
if (prv_should_show_action_in_action_menu(window_data, item, action)) {
num_timeline_actions++;
if (timeline_item_action_is_dismiss(action)) {
dismiss_action = action;
}
}
}
// Snooze is not needed for Reminders App items
Uuid items_originator_id;
timeline_get_originator_id(item, &items_originator_id);
const bool has_snooze_action = ((item->header.type == TimelineItemTypeReminder) &&
!uuid_equal(&(Uuid)UUID_REMINDERS_DATA_SOURCE, &items_originator_id) &&
reminders_can_snooze(item));
const bool has_dismiss_all_action = ((dismiss_action) &&
(notifications_presented_list_count() > 1));
const bool has_quiet_time_action = true; // Always true
const bool has_ancs_mute_action = prv_has_mute_action(item);
uint8_t num_local_actions = 0;
num_local_actions += (has_snooze_action) ? 1 : 0;
num_local_actions += (has_dismiss_all_action) ? 1 : 0;
num_local_actions += (has_quiet_time_action) ? 1 : 0;
num_local_actions += (has_ancs_mute_action) ? 1 : 0;
uint8_t num_item_specific_actions = num_timeline_actions;
num_item_specific_actions += (has_snooze_action) ? 1 : 0;
num_item_specific_actions += (has_ancs_mute_action) ? 1 : 0;
// Create root level
uint8_t num_actions = num_timeline_actions + num_local_actions;
uint8_t separator_index = num_actions > num_item_specific_actions ? num_item_specific_actions : 0;
ActionMenuLevel *root_level = timeline_actions_create_action_menu_root_level(num_actions,
separator_index,
source);
// Add actions in order
// [0] Dismiss (if applicable)
// [1] Snooze (if applicable)
// [2] Other mobile actions
// ... Other mobile actions
// [n] Other mobile actions
// [n + 1] ANCS Mute (if applicable)
// [n + 2] Dismiss all (if applicable)
// [n + 3] Toggle Quiet Time
if (dismiss_action) {
timeline_actions_add_action_to_root_level(dismiss_action, root_level);
}
if (has_snooze_action) {
action_menu_level_add_action(root_level,
i18n_get("Snooze", root_level),
prv_snooze_reminder_cb,
window_data);
}
for (int i = 0; i < item->action_group.num_actions; i++) {
TimelineItemAction *action = &item->action_group.actions[i];
if (prv_should_show_action_in_action_menu(window_data, item, action) &&
action != dismiss_action) {
timeline_actions_add_action_to_root_level(action, root_level);
}
}
if (has_ancs_mute_action) {
const char *app_id = attribute_get_string(&item->attr_list, AttributeIdiOSAppIdentifier, "");
iOSNotifPrefs *notif_prefs = ios_notif_pref_db_get_prefs((uint8_t *)app_id, strlen(app_id));
char *display_name = "";
if (notif_prefs) {
display_name = (char *)attribute_get_string(&notif_prefs->attr_list, AttributeIdAppName, "");
}
const char *mute_label = i18n_noop("Mute %s");
static char mute_label_buf[32];
snprintf(mute_label_buf, sizeof(mute_label_buf),
i18n_get(mute_label, root_level), display_name);
const uint8_t mute_option = ancs_filtering_get_mute_type(notif_prefs);
const bool is_mute_weekdays = mute_option == MuteBitfield_Weekdays;
const bool is_mute_weekends = mute_option == MuteBitfield_Weekends;
if (is_mute_weekdays || is_mute_weekends) {
action_menu_level_add_action(root_level,
mute_label_buf,
prv_mute_notification_always,
window_data);
} else {
const uint8_t number_mute_actions = 3;
ActionMenuLevel *mute_level = action_menu_level_create(number_mute_actions);
action_menu_level_add_child(root_level,
mute_level,
mute_label_buf);
action_menu_level_add_action(mute_level,
i18n_get("Mute Always", root_level),
prv_mute_notification_always,
window_data);
action_menu_level_add_action(mute_level,
i18n_get("Mute Weekends", root_level),
prv_mute_notification_weekends,
window_data);
action_menu_level_add_action(mute_level,
i18n_get("Mute Weekdays", root_level),
prv_mute_notification_weekdays,
window_data);
}
ios_notif_pref_db_free_prefs(notif_prefs);
}
if (has_dismiss_all_action) {
action_menu_level_add_action(root_level,
i18n_get("Dismiss All", root_level),
prv_dismiss_all_action_cb,
window_data);
}
if (has_quiet_time_action) {
action_menu_level_add_action(root_level,
do_not_disturb_is_active() ?
i18n_get("End Quiet Time", root_level) :
i18n_get("Start Quiet Time", root_level),
prv_toggle_dnd_from_action_menu,
window_data);
}
return root_level;
}
static void prv_action_menu_did_close(ActionMenu *action_menu, const ActionMenuItem *item,
void *context) {
NotificationWindowData *data = &s_notification_window_data;
data->action_menu = NULL;
}
static void prv_select_single_click_handler(ClickRecognizerRef recognizer, void *data) {
NotificationWindowData *window_data = data;
TimelineItem *item = prv_get_current_notification(window_data);
if (!prv_should_provide_action_menu_for_item(window_data, item)) {
return;
}
LayoutLayer *layout = swap_layer_get_current_layout(&window_data->swap_layer);
const LayoutColors *colors = layout_get_notification_colors(layout);
ActionMenuConfig config = {
.context = item,
.colors.background = colors->bg_color,
.did_close = prv_action_menu_did_close,
};
TimelineItemActionSource source = (window_data->is_modal ?
TimelineItemActionSourceModalNotification : TimelineItemActionSourceNotificationApp);
config.root_level = prv_create_action_menu_for_item(item, window_data, source);
if (config.root_level == NULL) {
PBL_LOG(LOG_LEVEL_ERROR, "Couldn't create notification action menu");
return;
}
window_data->action_menu = timeline_actions_push_action_menu(
&config, window_manager_get_window_stack(NOTIFICATION_PRIORITY));
}
static void prv_select_long_click_handler(ClickRecognizerRef recognizer, void *data) {
prv_dismiss_all(data, NULL);
}
static void prv_back_button_single_click_handler(ClickRecognizerRef recognizer, void *data) {
NotificationWindowData *window_data = data;
if (window_data->is_modal) {
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_CLOSED_COUNT, AnalyticsClient_System);
}
prv_pop_notification_window(window_data);
}
static void prv_click_config_provider(void *data) {
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_single_click_handler);
window_long_click_subscribe(BUTTON_ID_SELECT, 1000, prv_select_long_click_handler, NULL);
window_set_click_context(BUTTON_ID_SELECT, data);
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_button_single_click_handler);
window_set_click_context(BUTTON_ID_BACK, data);
if (s_notification_window_data.is_modal) {
Window *window = &s_notification_window_data.window;
window_set_overrides_back_button(window, true);
ClickManager *mgr = modal_manager_get_click_manager();
ClickConfig *cfg = &mgr->recognizers[BUTTON_ID_BACK].config;
cfg->long_click.delay_ms = QUICK_DND_HOLD_MS;
cfg->long_click.handler = prv_toggle_dnd_from_back_click;
}
}
///////////////////////
// Window Callbacks
///////////////////////
static void prv_window_appear(Window *window) {
// check if we still have any notifications to display. If not, pop!
NotificationWindowData *data = window_get_user_data(window);
if (notifications_presented_list_current_idx() < 0) {
prv_pop_notification_window_after_delay(data, 0);
return;
}
prv_setup_reminder_watchdog(data);
prv_refresh_pop_timer(data);
// update status bar to the current info
prv_update_status_layer(data);
// Reload notification data from notification_storage in case of an action/remove
if (data->notifications_modified) {
data->notifications_modified = false;
prv_reload_swap_layer(data);
}
}
static void prv_window_disappear(Window *window) {
NotificationWindowData *data = window_get_user_data(window);
prv_cleanup_timer(&data->pop_timer_id);
}
static void prv_handle_presented_notif_deinit(Uuid *id, NotificationType type, void *not_used) {
if (type == NotificationReminder) {
// the reminder has been shown so delete it.
// don't send an event, because there might be more reminders than queue slots
reminder_db_delete_item(id, false /* send_event */);
}
}
static void prv_window_unload(Window *window) {
NotificationWindowData *data = window_get_user_data(window);
if (!data) {
return;
}
vibes_cancel();
prv_cleanup_timers(data);
// clean up peek layer
if (data->peek_layer) {
peek_layer_destroy(data->peek_layer);
data->peek_layer = NULL;
}
animation_unschedule(data->peek_animation);
swap_layer_deinit(&data->swap_layer);
status_bar_layer_deinit(&data->status_layer);
notifications_presented_list_deinit(prv_handle_presented_notif_deinit, NULL);
gbitmap_deinit(&data->dnd_icon);
layer_deinit(&data->dnd_icon_layer);
window_deinit(window);
i18n_free_all(data);
s_in_use = false;
}
//////////////////////
// Callback Handlers
//////////////////////
static void prv_layout_did_appear_handler(SwapLayer *swap_layer, LayoutLayer *layout,
int8_t rel_change, void *context) {
NotificationWindowData *data = context;
TimelineItem *n = layout_get_context(layout);
Uuid *id = &n->header.id;
notifications_presented_list_set_current(id);
if (data->first_notif_loaded || !data->is_modal) {
layer_set_hidden(&data->action_button_layer, !prv_should_provide_action_menu_for_item(data, n));
}
// update status bar to the current info
prv_update_status_layer(data);
kino_layer_play(&((NotificationLayout *)layout)->icon_layer);
}
static void prv_update_colors_handler(SwapLayer *swap_layer, GColor bg_color,
bool status_bar_filled, void *context) {
NotificationWindowData *data = context;
GColor status_color = (status_bar_filled) ? bg_color : GColorWhite;
// Status bar is clear on round, because the banner is rendered under it
status_bar_layer_set_colors(&data->status_layer, PBL_IF_ROUND_ELSE(GColorClear, status_color),
gcolor_legible_over(status_color));
}
static void prv_interaction_handler(SwapLayer *swap_layer, void *context) {
NotificationWindowData *data = context;
prv_refresh_pop_timer(data);
}
static void prv_set_dnd_icon_visible(bool is_visible) {
NotificationWindowData *data = &s_notification_window_data;
if (is_visible == data->dnd_icon_visible) {
// nothing to do here
return;
}
const GRect icon_rect = gbitmap_get_bounds(&data->dnd_icon);
#if PBL_ROUND
GRect new_status_frame = data->status_layer.layer.frame;
const int16_t icon_text_horizontal_spacing = 4;
const int16_t window_bounds_width = window_get_root_layer(&data->window)->bounds.size.w;
const int16_t title_width = status_layer_get_title_text_width(&data->status_layer);
const int16_t status_offset = (icon_rect.size.w + icon_text_horizontal_spacing) / 2;
new_status_frame.origin.x += is_visible ? status_offset : -status_offset;
const int16_t new_icon_layer_x_offset = ((window_bounds_width - title_width) / 2) - status_offset;
layer_set_frame(&data->status_layer.layer, &new_status_frame);
#endif
const uint16_t icon_layer_x_offset = PBL_IF_ROUND_ELSE(new_icon_layer_x_offset, 6);
const uint16_t icon_layer_y_offset = PBL_IF_ROUND_ELSE(10, 2);
const GRect dnd_frame = (GRect) {
.origin = GPoint(icon_layer_x_offset, icon_layer_y_offset),
.size = icon_rect.size,
};
layer_set_frame(&data->dnd_icon_layer, &dnd_frame);
data->dnd_icon_visible = is_visible;
}
static void prv_dnd_status_changed(bool dnd_is_active) {
if (s_notification_window_data.is_modal && s_in_use) {
prv_set_dnd_icon_visible(dnd_is_active);
}
}
///////////////////////////
// Notification Window API
///////////////////////////
static void prv_init_notification_window(bool is_modal) {
NotificationWindowData *data = &s_notification_window_data;
// init_notification_window() can be called from KernelMain when displaying an incoming
// notification and also from the notifications.c application task. Grab a mutex here so
// that we don't ever get two instances of it at a time.
mutex_lock(s_notification_window_mutex);
if (s_in_use) {
goto fail;
}
s_in_use = true;
data->pop_timer_is_final = false;
data->is_modal = is_modal;
data->notification_app_id = UUID_INVALID;
data->peek_layer_timer = EVENTED_TIMER_INVALID_ID;
data->peek_animation = NULL;
data->peek_layer = NULL;
data->peek_icon_info = (TimelineResourceInfo) {
.res_id = TIMELINE_RESOURCE_INVALID,
.app_id = NULL,
.fallback_id = TIMELINE_RESOURCE_INVALID
};
data->action_menu = NULL;
data->dnd_icon_visible = false;
Window *window = &data->window;
window_init(window, "Notification Window");
window_set_window_handlers(window, &(WindowHandlers) {
.appear = prv_window_appear,
.disappear = prv_window_disappear,
.unload = prv_window_unload,
});
window_set_user_data(window, data);
// Initialize some variables early
Layer *root_layer = window_get_root_layer(window);
const GRect *window_frame = &root_layer->frame;
// prepare the swap layer frame using notification_layout values including the status bar
const GRect swap_frame = GRect(0, STATUS_BAR_LAYER_HEIGHT, window_frame->size.w,
LAYOUT_HEIGHT + LAYOUT_ARROW_HEIGHT);
SwapLayer *swap_layer = &data->swap_layer;
swap_layer_init(swap_layer, &swap_frame);
swap_layer_set_callbacks(swap_layer, data, (SwapLayerCallbacks) {
.get_layout_handler = prv_get_layout_handler,
.layout_removed_handler = prv_layout_removed_handler,
.layout_did_appear_handler = prv_layout_did_appear_handler,
#if PBL_COLOR
.update_colors_handler = prv_update_colors_handler,
#endif
.interaction_handler = prv_interaction_handler,
.click_config_provider = prv_click_config_provider,
});
swap_layer_set_click_config_onto_window(swap_layer, window);
layer_add_child(root_layer, swap_layer_get_layer(swap_layer));
StatusBarLayer *status_layer = &data->status_layer;
status_bar_layer_init(status_layer);
status_bar_layer_set_colors(status_layer, PBL_IF_RECT_ELSE(GColorBlack, GColorClear),
PBL_IF_RECT_ELSE(GColorWhite, GColorBlack));
status_bar_layer_set_separator_mode(status_layer, StatusBarLayerSeparatorModeNone);
layer_add_child(root_layer, (Layer *)status_layer);
// bubble on right for action button
layer_init(&data->action_button_layer, &data->window.layer.bounds);
data->action_button_layer.update_proc = action_button_update_proc;
layer_add_child(root_layer, &data->action_button_layer);
layer_set_hidden((Layer *)&data->action_button_layer, true);
// Ideally this gets moved into the status layer in the future. See data struct comment
gbitmap_init_with_resource(&data->dnd_icon, RESOURCE_ID_QUIET_TIME_STATUS_BAR);
// actual frame of the icon layer is calculated in prv_set_dnd_icon_visible()
layer_init(&data->dnd_icon_layer, &GRectZero);
layer_set_update_proc(&data->dnd_icon_layer, prv_draw_dnd_icon);
layer_add_child(root_layer, &data->dnd_icon_layer);
prv_dnd_status_changed(do_not_disturb_is_active());
// set up the notification presented list service
notifications_presented_list_init();
fail:
mutex_unlock(s_notification_window_mutex);
}
void notification_window_init(bool is_modal) {
prv_init_notification_window(is_modal);
if (is_modal && notification_window_is_modal()) {
// If we didn't ask for a modal window, it means some other task already created it,
// so no need to push it
modal_window_push(&s_notification_window_data.window,
NOTIFICATION_PRIORITY, true /* animated */);
}
}
void notification_window_show() {
if (s_notification_window_data.is_modal) {
return;
}
const bool animated = true;
app_window_stack_push(&s_notification_window_data.window, animated);
}
bool notification_window_is_modal(void) {
return s_notification_window_data.is_modal;
}
void notification_window_add_notification_by_id(Uuid *id) {
prv_notification_window_add_notification(id, NotificationMobile);
}
//! The animate mode slides the notification in from the top as if it was a new notification.
void notification_window_focus_notification(Uuid *id, bool animated) {
NotificationWindowData *data = &s_notification_window_data;
if (animated) {
#if PBL_RECT
Uuid *second_id = notifications_presented_list_relative(
notifications_presented_list_first(), +1);
if (second_id) {
// On rectangular displays, get the notification below the one we want to focus,
// set it as the current notification, then swap up. This allows us
// to accomplish the animation effect, while still pleasing the SwapLayer
// when it wants to retrieve the layouts it wants to.
notifications_presented_list_set_current(second_id);
swap_layer_attempt_layer_swap(&data->swap_layer, ScrollDirectionUp);
return;
}
#else
// On round displays, just set the new notification as the current one and show
// the peek animation
notifications_presented_list_set_current(id);
prv_show_peek_for_notification(data, id, false /* is_first_notification */);
return;
#endif
}
// Animated was set to false or there was no notification after the focusing one.
// Just set the current notification and reload data.
notifications_presented_list_set_current(id);
prv_reload_swap_layer(data);
}
void notification_window_service_init(void) {
s_notification_window_mutex = mutex_create();
s_notification_window_data.pop_timer_id = EVENTED_TIMER_INVALID_ID;
}
//////////////////
// Event Handlers
//////////////////
static void prv_handle_action_result(PebbleSysNotificationActionResult *action_result) {
if (action_result->type != ActionResultTypeSuccess &&
action_result->type != ActionResultTypeSuccessANCSDismiss) {
return;
}
// the notification has been acted on. Remove it.
NotificationWindowData *data = &s_notification_window_data;
notification_storage_set_status(&action_result->id, TimelineItemStatusActioned);
data->notifications_modified = true;
if (data->is_modal) {
// Don't remove the action menu here. The timeline actions module also handles this event
// and will remove it as well as put a result dialog
prv_remove_notification(data, &action_result->id, false /* close am */);
prv_refresh_pop_timer(data);
}
}
static void prv_handle_notification_removed_common(Uuid *id, NotificationType type) {
NotificationWindowData *data = &s_notification_window_data;
if (s_in_use && data->is_modal && !data->window_frozen) {
prv_remove_notification(data, id, true /* close am */);
}
}
static void prv_handle_notification_acted_upon(Uuid *id) {
NotificationWindowData *data = &s_notification_window_data;
if (data->is_modal) {
prv_remove_notification(data, id, true /* close am */);
} else {
data->notifications_modified = true;
}
}
static void prv_handle_notification_added_common(Uuid *id, NotificationType type) {
if (!alerts_should_notify_for_type(prv_alert_type_for_notification_type(type))) {
return;
}
NotificationWindowData *data = &s_notification_window_data;
// will fail and return early if already init'ed.
prv_init_notification_window(true /*is_modal*/);
if (data->is_modal) {
WindowStack *window_stack = modal_manager_get_window_stack(NOTIFICATION_PRIORITY);
bool is_new = !window_stack_contains_window(window_stack, &data->window);
bool in_view = window_is_on_screen(&data->window);
prv_notification_window_add_notification(id, type);
if (is_new) {
data->first_notif_loaded = false;
prv_show_peek_for_notification(data, id, true /* is_first_notification */);
modal_window_push(&data->window, NOTIFICATION_PRIORITY, true /* animated */);
} else if (in_view) {
// Only focus the new notification if it becomes the new front of the list.
// In DND mode notifications can get inserted into the middle of the list and we don't
// want to change focus in this use case
if (notifications_presented_list_current() != notifications_presented_list_first()) {
const bool should_animate = !do_not_disturb_is_active();
notification_window_focus_notification(id, should_animate);
} else {
// If we are inserting into the middle of this list, just reload the swap layer so the
// number of notifications displayed is correct
prv_reload_swap_layer(data);
}
}
}
alerts_incoming_alert_analytics();
if (alerts_should_vibrate_for_type(prv_alert_type_for_notification_type(type))) {
#if CAPABILITY_HAS_VIBE_SCORES
VibeScore *score = vibe_client_get_score(VibeClient_Notifications);
if (score) {
vibe_score_do_vibe(score);
vibe_score_destroy(score);
}
#else
vibes_short_pulse();
#endif
// Timestamp set after call to vibrate since if something fails,
// its better to have no vibe blocking then vibe blocking and no vibrations.
alerts_set_notification_vibe_timestamp();
}
if (alerts_should_enable_backlight_for_type(prv_alert_type_for_notification_type(type))) {
light_enable_interaction();
}
prv_refresh_pop_timer(data);
}
static bool prv_is_item_loaded(Uuid *id) {
return (uuid_equal(id, notifications_presented_list_current()) ||
uuid_equal(id, notifications_presented_list_next()));
}
static void prv_handle_reminder_updated(Uuid *id) {
NotificationWindowData *data = &s_notification_window_data;
// We only need to reload from flash if the item is already in memory
// (ie. the current item or the next/peek item)
if (data->is_modal && prv_is_item_loaded(id)) {
prv_reload_swap_layer(data);
}
}
void notification_window_handle_reminder(PebbleReminderEvent *e) {
switch (e->type) {
case ReminderTriggered:
prv_handle_notification_added_common(e->reminder_id, NotificationReminder);
break;
case ReminderRemoved:
prv_handle_notification_removed_common(e->reminder_id, NotificationReminder);
break;
case ReminderUpdated:
prv_handle_reminder_updated(e->reminder_id);
break;
}
}
void notification_window_handle_notification(PebbleSysNotificationEvent *e) {
if (!s_in_use && e->type != NotificationAdded) {
return;
}
switch (e->type) {
case NotificationActionResult:
prv_handle_action_result(e->action_result);
break;
case NotificationAdded:
prv_handle_notification_added_common(e->notification_id, NotificationMobile);
break;
case NotificationActedUpon:
prv_handle_notification_acted_upon(e->notification_id);
break;
case NotificationRemoved:
prv_handle_notification_removed_common(e->notification_id, NotificationMobile);
break;
}
}
void notification_window_handle_dnd_event(PebbleDoNotDisturbEvent *e) {
if (!s_notification_window_data.is_modal || !s_in_use) {
return;
}
prv_dnd_status_changed(e->is_active);
if (e->is_active) {
prv_setup_reminder_watchdog(&s_notification_window_data);
} else {
prv_cancel_reminder_watchdog(&s_notification_window_data);
}
if (prv_should_pop_due_to_inactivity()) {
// Re-schedule the window pop timer after leaving DND mode
prv_refresh_pop_timer(&s_notification_window_data);
}
}
// This function is only used by the notifications_app.
// When it calls this function, it knows it is a valid notification already.
void app_notification_window_add_new_notification_by_id(Uuid *id) {
if (do_not_disturb_is_active()) {
return;
}
NotificationWindowData *data = &s_notification_window_data;
if (!s_in_use || data->is_modal) {
return;
}
bool should_focus = (app_window_stack_get_top_window() == &data->window);
notification_window_add_notification_by_id(id);
if (should_focus) {
const bool animated = true;
notification_window_focus_notification(id, animated);
}
}
// This function is only used by the notifications_app.
// When it calls this function, it knows it is a valid notification already.
void app_notification_window_remove_notification_by_id(Uuid *id) {
NotificationWindowData *data = &s_notification_window_data;
if (!s_in_use || data->is_modal) {
return;
}
prv_remove_notification(data, id, true /* close am */);
}
// This function is only used by the notifications_app.
// When it calls this function, it knows it is a valid notification already.
void app_notification_window_handle_notification_acted_upon_by_id(Uuid *id) {
NotificationWindowData *data = &s_notification_window_data;
if (!s_in_use || data->is_modal) {
return;
}
prv_reload_swap_layer(data);
}