mirror of
https://github.com/google/pebble.git
synced 2025-07-20 22:33:54 -04:00
818 lines
30 KiB
C
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(¬ification->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, ¬ification)) {
|
|
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(¬ification.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, ¬ification.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(
|
|
¬ifications_data->loaded_notification_list, node);
|
|
if (!loaded_node) {
|
|
return;
|
|
}
|
|
|
|
TimelineItem *notification = &loaded_node->notification;
|
|
const char *title = attribute_get_string(¬ification->attr_list, AttributeIdTitle, "");
|
|
const char *subtitle = attribute_get_string(¬ification->attr_list, AttributeIdSubtitle, "");
|
|
const char *app_name = attribute_get_string(¬ification->attr_list, AttributeIdAppName, "");
|
|
const char *body = attribute_get_string(¬ification->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, ¬ification)) {
|
|
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;
|
|
}
|