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

818 lines
30 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 "notifications_app.h"
#include <stdio.h>
#include <time.h>
#include "applib/app.h"
#include "applib/app_exit_reason.h"
#include "applib/preferred_content_size.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gdraw_command_list.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/menu_cell_layer.h"
#include "applib/ui/ui.h"
#include "applib/ui/window_stack_private.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/system_icons.h"
#include "popups/notifications/notification_window.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/blob_db/pin_db.h"
#include "services/normal/notifications/notification_storage.h"
#include "services/normal/timeline/notification_layout.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/date.h"
#include "util/list.h"
#include "util/string.h"
#if !TINTIN_FORCE_FIT
typedef struct LoadedNotificationNode {
ListNode node;
TimelineItem notification;
GDrawCommandImage *icon;
bool icon_is_default;
} LoadedNotificationNode;
typedef struct NotificationNode {
ListNode node;
Uuid id;
} NotificationNode;
typedef struct NotificationsData {
Window window;
MenuLayer menu_layer;
TextLayer text_layer;
NotificationNode *notification_list;
LoadedNotificationNode *loaded_notification_list;
EventServiceInfo notification_event_info;
ActionableDialog *actionable_dialog;
#if PBL_ROUND
StatusBarLayer status_bar_layer;
#endif
} NotificationsData;
static NotificationsData *s_data = NULL;
static const unsigned int MAX_ACTIVE_NOTIFICATIONS = 6;
static bool prv_loaded_notification_list_filter_cb(ListNode *node, void *data) {
LoadedNotificationNode *loaded_notification = (LoadedNotificationNode *)node;
Uuid *id = data;
return uuid_equal(&loaded_notification->notification.header.id, id);
}
static bool prv_notification_list_filter_cb(ListNode *node, void *data) {
NotificationNode *notification = (NotificationNode *)node;
Uuid *id = data;
return uuid_equal(&notification->id, id);
}
static NotificationNode *prv_find_notification(NotificationNode *list, Uuid *id) {
return (NotificationNode *)list_find((ListNode *)list,
prv_notification_list_filter_cb,
id);
}
static LoadedNotificationNode *prv_find_loaded_notification(LoadedNotificationNode *list,
Uuid *id) {
return (LoadedNotificationNode *)list_find((ListNode *)list,
prv_loaded_notification_list_filter_cb,
id);
}
static NotificationNode *prv_notification_list_add_notification_by_id(
NotificationNode **notification_list, Uuid *id) {
NotificationNode *new_node = app_malloc_check(sizeof(NotificationNode));
list_init((ListNode*) new_node);
new_node->id = *id;
*notification_list = (NotificationNode*) list_prepend((ListNode*) *notification_list,
(ListNode*) new_node);
return new_node;
}
static void prv_notification_list_remove_notification_by_id(
NotificationNode **notification_list, Uuid *id) {
NotificationNode *node = prv_find_notification(*notification_list, id);
list_remove((ListNode *)node, (ListNode **)notification_list, NULL);
}
static NotificationNode *prv_add_notification(NotificationsData *data, Uuid *id) {
NotificationNode *node = prv_notification_list_add_notification_by_id(&data->notification_list,
id);
return node;
}
static void prv_remove_notification(NotificationsData *data, Uuid *id) {
prv_notification_list_remove_notification_by_id(&data->notification_list, id);
}
static bool prv_notif_iterator_callback(void *data, SerializedTimelineItemHeader *header) {
return (prv_add_notification(data, &header->common.id) != NULL);
}
static void prv_load_notification_storage(NotificationsData *data) {
notification_storage_iterate(&prv_notif_iterator_callback, data);
}
static void prv_notification_list_deinit(NotificationNode *notification_list) {
while (notification_list) {
NotificationNode *node = notification_list;
notification_list = (NotificationNode*) list_pop_head((ListNode*) notification_list);
app_free(node);
}
}
static void prv_unload_loaded_notification(LoadedNotificationNode *loaded_notif) {
timeline_item_free_allocated_buffer(&loaded_notif->notification);
gdraw_command_image_destroy(loaded_notif->icon);
app_free(loaded_notif);
}
static NOINLINE LoadedNotificationNode *prv_loaded_notification_list_load_item(
LoadedNotificationNode **loaded_list, NotificationNode *node) {
if (node == NULL) {
return NULL;
}
LoadedNotificationNode *loaded_node = prv_find_loaded_notification(*loaded_list, &node->id);
if (loaded_node) {
return loaded_node;
}
// unload old notifications
if (list_count((ListNode*) *loaded_list) > MAX_ACTIVE_NOTIFICATIONS) {
LoadedNotificationNode *old_node = (LoadedNotificationNode*) list_get_tail(
(ListNode*) *loaded_list);
list_remove((ListNode*) old_node, (ListNode**) loaded_list, NULL);
prv_unload_loaded_notification(old_node);
}
// load the notification
TimelineItem notification;
if (!notification_storage_get(&node->id, &notification)) {
return NULL;
}
// track the loaded notification
loaded_node = app_malloc_check(sizeof(LoadedNotificationNode));
list_init((ListNode*) loaded_node);
loaded_node->notification = notification;
TimelineResourceId timeline_res_id = attribute_get_uint32(&notification.attr_list,
AttributeIdIconTiny,
NOTIF_FALLBACK_ICON);
// Read the associated pin's app id
TimelineItem pin;
if (timeline_resources_is_system(timeline_res_id) ||
pin_db_read_item_header(&pin, &notification.header.parent_id) != S_SUCCESS) {
pin.header.parent_id = (Uuid)UUID_INVALID;
}
TimelineResourceInfo timeline_res = {
.res_id = timeline_res_id,
.app_id = &pin.header.parent_id,
.fallback_id = NOTIF_FALLBACK_ICON
};
AppResourceInfo icon_res_info;
timeline_resources_get_id(&timeline_res, TimelineResourceSizeTiny, &icon_res_info);
loaded_node->icon = gdraw_command_image_create_with_resource_system(icon_res_info.res_app_num,
icon_res_info.res_id);
loaded_node->icon_is_default = (timeline_res_id == NOTIF_FALLBACK_ICON) ||
(timeline_res_id == TIMELINE_RESOURCE_NOTIFICATION_GENERIC);
*loaded_list = (LoadedNotificationNode*) list_prepend((ListNode*) *loaded_list,
(ListNode*)loaded_node);
return loaded_node;
}
static void prv_loaded_notification_list_deinit(LoadedNotificationNode *loaded_list) {
while (loaded_list) {
LoadedNotificationNode *node = loaded_list;
loaded_list = (LoadedNotificationNode*) list_pop_head((ListNode*) loaded_list);
prv_unload_loaded_notification(node);
}
}
// Return true if successful
static bool prv_push_notification_window(NotificationsData *data) {
notification_window_init(false /*is_modal*/);
// Bail if a notification came in ahead of us and created a modal window
// before we had a chance to react to the select button event.
if (notification_window_is_modal()) {
return false;
}
// iterate over visible items as visible (including the groups) in reverse order
// since notification_window shows each newly added notification first
NotificationNode *node = (NotificationNode*)list_get_tail(&data->notification_list->node);
while (node) {
notification_window_add_notification_by_id(&node->id);
node = (NotificationNode*)list_get_prev(&node->node);
}
notification_window_show();
return true;
}
///////////////////
// Confirm Dialog
static void prv_dialog_unloaded(void *context) {
NotificationsData *data = context;
data->actionable_dialog = NULL;
}
static void prv_confirmed_handler(ClickRecognizerRef recognizer, void *context) {
NotificationsData *data = context;
notification_storage_reset_and_init();
prv_loaded_notification_list_deinit(data->loaded_notification_list);
data->loaded_notification_list = NULL;
prv_notification_list_deinit(data->notification_list);
data->notification_list = NULL;
prv_load_notification_storage(data);
actionable_dialog_pop(data->actionable_dialog);
// Create and display DONE dialog
SimpleDialog *confirmation_dialog = simple_dialog_create("Notifications Cleared");
Dialog *dialog = simple_dialog_get_dialog(confirmation_dialog);
dialog_set_text(dialog, i18n_get("Done", data));
dialog_set_icon(dialog, RESOURCE_ID_RESULT_SHREDDED_LARGE);
static const uint32_t DIALOG_TIMEOUT = 2000;
dialog_set_timeout(dialog, DIALOG_TIMEOUT);
// Set the app exit reason so we will go to the watchface upon exit
app_exit_reason_set(APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY);
// Pop all windows so we'll soon exit the app
app_window_stack_pop_all(true /* animated */);
// Immediately push this result dialog so it's the last thing we see before exiting
app_simple_dialog_push(confirmation_dialog);
}
static void prv_dialog_click_config(void *context) {
NotificationsData *data = app_state_get_user_data();
window_single_click_subscribe(BUTTON_ID_SELECT, prv_confirmed_handler);
window_set_click_context(BUTTON_ID_SELECT, data);
}
static void prv_settings_clear_history_window_push(NotificationsData *data) {
ActionableDialog *actionable_dialog = actionable_dialog_create("Clear Notifications");
actionable_dialog_set_click_config_provider(actionable_dialog, prv_dialog_click_config);
actionable_dialog_set_action_bar_type(actionable_dialog, DialogActionBarConfirm, NULL);
Dialog *dialog = actionable_dialog_get_dialog(actionable_dialog);
dialog_set_text(dialog, i18n_get("Clear history?", data));
TimelineResourceInfo timeline_res = {
.res_id = TIMELINE_RESOURCE_GENERIC_QUESTION,
};
AppResourceInfo icon_res_info;
timeline_resources_get_id(&timeline_res, TimelineResourceSizeLarge, &icon_res_info);
dialog_set_icon(dialog, icon_res_info.res_id);
dialog_set_icon_animate_direction(dialog, DialogIconAnimationFromRight);
dialog_set_callbacks(dialog, &(DialogCallbacks) {
.unload = prv_dialog_unloaded,
}, data);
app_actionable_dialog_push(actionable_dialog);
data->actionable_dialog = actionable_dialog;
}
static GColor prv_invert_bw_color(GColor color) {
if (gcolor_equal(color, GColorBlack)) {
return GColorWhite;
} else if (gcolor_equal(color, GColorWhite)) {
return GColorBlack;
}
return color;
}
static void prv_invert_pdc_colors(GDrawCommandProcessor *processor,
GDrawCommand *processed_command,
size_t processed_command_max_size,
const GDrawCommandList* list,
const GDrawCommand *command) {
gdraw_command_set_stroke_color(processed_command,
prv_invert_bw_color(gdraw_command_get_stroke_color((GDrawCommand *)command)));
gdraw_command_set_fill_color(processed_command,
prv_invert_bw_color(gdraw_command_get_fill_color((GDrawCommand *)command)));
}
static void prv_draw_pdc_bw_inverted(GContext *ctx, GDrawCommandImage *image, GPoint offset) {
GDrawCommandProcessor processor = {
.command = prv_invert_pdc_colors,
};
gdraw_command_image_draw_processed(ctx, image, offset, &processor);
}
//////////////
// MenuLayer callbacks
static const uint8_t BAR_PX = 9;
static const uint8_t BAR_SELECTED_PX = 12;
static void prv_draw_notification_cell_rect(GContext *ctx, const Layer *cell_layer,
const char *title, const char *subtitle,
GDrawCommandImage *icon) {
const GRect cell_layer_bounds = cell_layer->bounds;
const GSize icon_size = gdraw_command_image_get_bounds_size(icon);
const int16_t icon_left_margin = menu_cell_basic_horizontal_inset();
if (icon) {
void (*draw_func)(GContext *, GDrawCommandImage *, GPoint) = gdraw_command_image_draw;
#if PBL_BW
if (menu_cell_layer_is_highlighted(cell_layer)) {
draw_func = prv_draw_pdc_bw_inverted;
}
#endif
// Inset the draw box from the left to leave some margin on the icon's left side
GRect box = cell_layer_bounds;
box.origin.x += icon_left_margin;
// Align the icon to the left of the draw box, centered vertically
GRect icon_rect = (GRect) { .size = gdraw_command_image_get_bounds_size(icon) };
grect_align(&icon_rect, &box, GAlignLeft, false /* clip */);
draw_func(ctx, icon, icon_rect.origin);
}
// Temporarily inset the cell layer's bounds from the left so the text doesn't draw over any
// icon on the left
Layer *mutable_cell_layer = (Layer *)cell_layer;
const int text_left_margin =
icon_left_margin + MAX(icon_size.w, ATTRIBUTE_ICON_TINY_SIZE_PX);
mutable_cell_layer->bounds = grect_inset(cell_layer_bounds,
GEdgeInsets(0, 5, 0, text_left_margin));
const GFont title_font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle);
const GFont subtitle_font = system_theme_get_font_for_default_size(TextStyleFont_Caption);
menu_cell_basic_draw_custom(ctx, cell_layer, title_font, title, NULL /* value_font */,
NULL /* value */, subtitle_font, subtitle, NULL /* icon */,
false /* icon_on_right */, GTextOverflowModeTrailingEllipsis);
// Restore the cell layer's bounds
mutable_cell_layer->bounds = cell_layer_bounds;
}
//! outer_box is passed as a pointer to save stack space
static int16_t prv_draw_centered_text_line_in(GContext *ctx, GFont font, const GRect *outer_box,
const char *text, GAlign align) {
if (!text) {
return 0;
}
GRect text_box = *outer_box;
text_box.size.h = fonts_get_font_height(font);
grect_align(&text_box, outer_box, align, true);
graphics_draw_text(ctx, text, font, text_box, GTextOverflowModeTrailingEllipsis,
GTextAlignmentCenter, NULL);
return text_box.size.h;
}
//! box is passed as a pointer to save stack space
//! after this call, box will point to the GRect where
//! the notification title was drawn
void prv_draw_notification_cell_round(GContext *ctx, const Layer *cell_layer, GRect *box,
GFont const title_font, const char *title,
GFont const subtitle_font, const char *subtitle,
GDrawCommandImage *icon) {
if (icon) {
GRect icon_rect = (GRect){.size = gdraw_command_image_get_bounds_size(icon)};
grect_align(&icon_rect, box, GAlignTop, true);
icon_rect.origin.y += 4;
gdraw_command_image_draw(ctx, icon, icon_rect.origin);
// more box by icon + some margin
const int16_t icon_space = icon_rect.origin.y + icon_rect.size.h - 12;
// manually inset to save stack space, instead of using grect_inset
box->origin.y += icon_space;
box->size.h -= icon_space;
}
// hack: compensate for text placement inside a rect
box->origin.y -= 4;
if (subtitle) {
box->size.h -= prv_draw_centered_text_line_in(ctx, subtitle_font, box, subtitle,
GAlignBottom);
}
if (title) {
prv_draw_centered_text_line_in(ctx, title_font, box, title, GAlignCenter);
}
}
static void prv_draw_notification_cell_round_selected(GContext *ctx, const Layer *cell_layer,
const char *title, const char *subtitle,
GDrawCommandImage *icon) {
// as measured from the design specs
const int inset = 8;
GRect frame = cell_layer->bounds;
// manually inset the frame to save stack space, instead of using grect_inset
frame.origin.x += inset;
frame.origin.y += inset;
frame.size.h -= inset * 2;
frame.size.w -= inset * 2;
const GFont title_font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle);
const GFont subtitle_font =
system_theme_get_font_for_default_size(TextStyleFont_MenuCellSubtitle);
prv_draw_notification_cell_round(ctx, cell_layer, &frame, title_font, title, subtitle_font,
subtitle, icon);
}
static void prv_draw_notification_cell_round_unselected(GContext *ctx, const Layer *cell_layer,
const char *title, const char *subtitle,
GDrawCommandImage *icon) {
// as measured from the design specs
const int horizontal_inset = MENU_CELL_ROUND_UNFOCUSED_HORIZONTAL_INSET;
const int top_inset = 2;
GRect frame = cell_layer->bounds;
// manually inset the frame to save stack space, instead of using grect_inset
frame.origin.x += horizontal_inset;
frame.size.w -= horizontal_inset * 2;
frame.origin.y += top_inset;
frame.size.h -= top_inset;
// Using TextStyleFont_Header here is a little bit of a hack to achieve Gothic 18 Bold on
// Spalding's default content size (medium) while still being a little robust for any future round
// watches that have a default content size larger than medium
const GFont font = system_theme_get_font_for_default_size(TextStyleFont_Header);
prv_draw_notification_cell_round(ctx, cell_layer, &frame, font, title, NULL, NULL, NULL);
}
static void prv_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index,
void *data) {
NotificationsData *notifications_data = data;
if ((notifications_data->notification_list) && (cell_index->row == 0)) {
// Clear All button selected
prv_settings_clear_history_window_push(notifications_data);
return;
}
// shift index since the first one is hard coded to Clear
int16_t notif_idx = cell_index->row - 1;
NotificationNode *node = (NotificationNode*) list_get_at(
(ListNode*) notifications_data->notification_list, notif_idx);
if (!node) {
return;
}
bool success = prv_push_notification_window(notifications_data);
if (!success) {
// Bail if a notification came in ahead of us and created a modal window
// before we had a chance to react to the select button event.
return;
}
const bool animated = false;
notification_window_focus_notification(&node->id, animated);
}
static uint16_t prv_get_num_rows_callback(struct MenuLayer *menu_layer, uint16_t section_index,
void *data) {
NotificationsData *notifications_data = data;
NotificationNode *node = notifications_data->notification_list;
// There's no notifications, don't draw anything
if (!node) {
return 0;
}
// add one for the CLEAR ALL at the top
return list_count((ListNode *)notifications_data->notification_list) + 1;
}
static int16_t prv_get_cell_height(struct MenuLayer *menu_layer, MenuIndex *cell_index,
void *data) {
#if PBL_ROUND
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
bool is_selected = menu_index_compare(cell_index, &selected_index) == 0;
if (is_selected) {
return MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT;
}
#endif
const PreferredContentSize runtime_platform_content_size =
system_theme_get_default_content_size_for_runtime_platform();
return ((int16_t[NumPreferredContentSizes]) {
//! @note this is the same as Medium until Small is designed
[PreferredContentSizeSmall] = PBL_IF_RECT_ELSE(46, MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT),
[PreferredContentSizeMedium] = PBL_IF_RECT_ELSE(46,
MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT),
[PreferredContentSizeLarge] = menu_cell_basic_cell_height(),
//! @note this is the same as Large until ExtraLarge is designed
[PreferredContentSizeExtraLarge] = menu_cell_basic_cell_height(),
})[runtime_platform_content_size];
}
static void prv_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index,
void *data) {
NotificationsData *notifications_data = data;
void (*draw_cell)(GContext *, const Layer *, const char *, const char *, GDrawCommandImage *) =
PBL_IF_RECT_ELSE(prv_draw_notification_cell_rect, prv_draw_notification_cell_round_selected);
#if PBL_ROUND
// on round: just draw the title for anything but the focused row
if (!menu_layer_is_index_selected(&s_data->menu_layer, cell_index)) {
draw_cell = prv_draw_notification_cell_round_unselected;
}
#endif
bool first_row = (cell_index->row == 0);
// Test if there are any notifications in the list.
if (first_row) {
// Draw "Clear all" box and exit
#if PBL_ROUND
draw_cell(ctx, cell_layer, i18n_get("Clear All", data), NULL, NULL);
#else
const GFont font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle);
GRect box = cell_layer->bounds;
box.origin.y += 6;
graphics_draw_text(ctx, i18n_get("Clear All", data), font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL);
#endif
return;
}
// shift index since the first one is hard coded to Clear
const int16_t notif_idx = cell_index->row - 1;
NotificationNode *node = (NotificationNode*) list_get_at(
(ListNode*) notifications_data->notification_list, notif_idx);
if (!node) {
return;
}
LoadedNotificationNode *loaded_node = prv_loaded_notification_list_load_item(
&notifications_data->loaded_notification_list, node);
if (!loaded_node) {
return;
}
TimelineItem *notification = &loaded_node->notification;
const char *title = attribute_get_string(&notification->attr_list, AttributeIdTitle, "");
const char *subtitle = attribute_get_string(&notification->attr_list, AttributeIdSubtitle, "");
const char *app_name = attribute_get_string(&notification->attr_list, AttributeIdAppName, "");
const char *body = attribute_get_string(&notification->attr_list, AttributeIdBody, "");
// We show the app name if we don't have a custom icon, otherwise we use the title
if (!IS_EMPTY_STRING(app_name) && loaded_node->icon_is_default) {
title = app_name;
}
if (!IS_EMPTY_STRING(title) && !IS_EMPTY_STRING(subtitle)) {
// we got a title & subtitle, we're done
} else if (IS_EMPTY_STRING(title) && IS_EMPTY_STRING(subtitle)) {
// we got neither, use the body
if (IS_EMPTY_STRING(body)) {
// we're screwed... empty message
title = "[Empty]";
} else {
// try to show as much content as possible in title + subtitle
title = body;
subtitle = strchr(body, '\n'); // NULL handled gracefully downstream
}
} else if (IS_EMPTY_STRING(title)) {
// no title, but yes subtitle.
title = subtitle;
subtitle = body;
} else if (IS_EMPTY_STRING(subtitle)) {
// no subtitle, but yes title
subtitle = body;
} else {
WTF;
}
draw_cell(ctx, cell_layer, title, subtitle, loaded_node->icon);
}
// Display the appropriate layer
static void prv_update_text_layer_visibility(NotificationsData *data) {
NotificationNode *node = data->notification_list;
// Toggle which layer is visible
if (node == NULL) {
layer_set_hidden((Layer *) &data->menu_layer, true);
layer_set_hidden((Layer *) &data->text_layer, false);
} else {
layer_set_hidden((Layer *) &data->menu_layer, false);
layer_set_hidden((Layer *) &data->text_layer, true);
}
}
static void prv_handle_notification_removed(Uuid *id) {
prv_remove_notification(s_data, id);
app_notification_window_remove_notification_by_id(id);
}
static void prv_handle_notification_acted_upon(Uuid *id) {
app_notification_window_handle_notification_acted_upon_by_id(id);
}
static void prv_handle_notification_added(Uuid *id) {
TimelineItem notification;
if (!notification_storage_get(id, &notification)) {
return;
}
prv_add_notification(s_data, id);
// NOTE: To avoid having two flash reads, we only read and validate the notification once.
// We do it here, instead of in the function call below. If the above
// notification_storage validation above is removed, then we should at least validate
// it in the function call below.
app_notification_window_add_new_notification_by_id(id);
}
static void prv_handle_notification(PebbleEvent *e, void *context) {
if (e->type == PEBBLE_SYS_NOTIFICATION_EVENT) {
Uuid *id = e->sys_notification.notification_id;
switch(e->sys_notification.type) {
case NotificationAdded:
prv_handle_notification_added(id);
break;
case NotificationRemoved:
prv_handle_notification_removed(id);
break;
case NotificationActedUpon:
prv_handle_notification_acted_upon(id);
break;
default:
break;
// Not implemented
}
menu_layer_reload_data(&s_data->menu_layer);
prv_update_text_layer_visibility(s_data);
}
// we don't handle reminders within the notifications app
}
///////////////////
// Window callbacks
static void prv_window_appear(Window *window) {
NotificationsData *data = window_get_user_data(window);
prv_update_text_layer_visibility(data);
}
static void prv_window_disappear(Window *window) {
NotificationsData *data = window_get_user_data(window);
prv_loaded_notification_list_deinit(data->loaded_notification_list);
data->loaded_notification_list = NULL;
}
static void prv_window_load(Window *window) {
NotificationsData *data = window_get_user_data(window);
MenuLayer *menu_layer = &data->menu_layer;
const GRect menu_layer_frame = PBL_IF_RECT_ELSE(
window->layer.bounds, grect_inset_internal(window->layer.bounds, 0, STATUS_BAR_LAYER_HEIGHT));
menu_layer_init(menu_layer, &menu_layer_frame);
menu_layer_set_callbacks(menu_layer, data, &(MenuLayerCallbacks) {
.get_num_rows = prv_get_num_rows_callback,
.draw_row = prv_draw_row_callback,
.get_cell_height = prv_get_cell_height,
.select_click = prv_select_callback,
});
menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack);
menu_layer_set_highlight_colors(menu_layer,
PBL_IF_COLOR_ELSE(DEFAULT_NOTIFICATION_COLOR, GColorBlack),
GColorWhite);
menu_layer_set_click_config_onto_window(menu_layer, window);
layer_add_child(&window->layer, menu_layer_get_layer(menu_layer));
TextLayer *text_layer = &data->text_layer;
const int16_t horizontal_margin = 5;
const GFont font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle);
// configure text layer to be vertically aligned (15 is hacking around our poor fonts)
text_layer_init_with_parameters(text_layer,
&GRect(horizontal_margin, window->layer.bounds.size.h / 2 - 15,
window->layer.bounds.size.w - horizontal_margin,
window->layer.bounds.size.h / 2),
i18n_get("No notifications", data), font, GColorBlack,
GColorWhite, GTextAlignmentCenter,
GTextOverflowModeTrailingEllipsis);
layer_add_child(&window->layer, text_layer_get_layer(text_layer));
#if PBL_ROUND
GColor bg_color = GColorClear;
GColor fg_color = GColorBlack;
StatusBarLayer *status_bar = &data->status_bar_layer;
status_bar_layer_init(status_bar);
status_bar_layer_set_colors(status_bar, bg_color, fg_color);
layer_add_child(&window->layer, &status_bar->layer);
#endif
menu_layer_set_selected_index(menu_layer, MenuIndex(0, 1),
PBL_IF_RECT_ELSE(MenuRowAlignNone, MenuRowAlignCenter), false);
}
static void prv_push_window(NotificationsData *data) {
Window *window = &data->window;
window_init(window, WINDOW_NAME("Notifications"));
window_set_user_data(window, data);
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_window_load,
.appear = prv_window_appear,
.disappear = prv_window_disappear,
});
const bool animated = true;
app_window_stack_push(window, animated);
}
////////////////////
// App boilerplate
static void prv_handle_init(void) {
NotificationsData *data = s_data = app_zalloc_check(sizeof(NotificationsData));
app_state_set_user_data(data);
data->notification_event_info = (EventServiceInfo) {
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
.handler = prv_handle_notification,
};
event_service_client_subscribe(&data->notification_event_info);
prv_load_notification_storage(data);
prv_push_window(data);
}
static void prv_handle_deinit(void) {
NotificationsData *data = app_state_get_user_data();
#if PBL_ROUND
status_bar_layer_deinit(&data->status_bar_layer);
#endif
menu_layer_deinit(&data->menu_layer);
event_service_client_unsubscribe(&data->notification_event_info);
prv_loaded_notification_list_deinit(data->loaded_notification_list);
prv_notification_list_deinit(data->notification_list);
i18n_free_all(data);
app_free(data);
s_data = NULL;
}
static void prv_s_main(void) {
prv_handle_init();
app_event_loop();
prv_handle_deinit();
}
#else
static void prv_s_main(void) {}
#endif
const PebbleProcessMd* notifications_app_get_info() {
static const PebbleProcessMdSystem s_app_md = {
.common = {
.main_func = prv_s_main,
// UUID: b2cae818-10f8-46df-ad2b-98ad2254a3c1
.uuid = {0xb2, 0xca, 0xe8, 0x18, 0x10, 0xf8, 0x46, 0xdf,
0xad, 0x2b, 0x98, 0xad, 0x22, 0x54, 0xa3, 0xc1},
},
.name = i18n_noop("Notifications"),
.icon_resource_id = RESOURCE_ID_NOTIFICATIONS_APP_GLANCE,
};
return (const PebbleProcessMd*) &s_app_md;
}