pebble/tests/fw/ui/test_notification_window.c
2025-01-27 11:38:16 -08:00

461 lines
16 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 "clar.h"
#include "applib/preferred_content_size.h"
#include "applib/ui/window_private.h"
#include "apps/system_apps/settings/settings_notifications_private.h"
#include "popups/notifications/notification_window.h"
#include "popups/notifications/notification_window_private.h"
#include "resource/timeline_resource_ids.auto.h"
#include "services/normal/timeline/notification_layout.h"
#include "util/trig.h"
#include <stdio.h>
// Stubs
/////////////////////
#include "stubs_action_menu.h"
#include "stubs_alarm_layout.h"
#include "stubs_alerts.h"
#include "stubs_analytics.h"
#include "stubs_ancs_filtering.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_bluetooth_persistent_storage.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_calendar_layout.h"
#include "stubs_click.h"
#include "stubs_content_indicator.h"
#include "stubs_dialog.h"
#include "stubs_do_not_disturb.h"
#include "stubs_event_loop.h"
#include "stubs_event_service_client.h"
#include "stubs_evented_timer.h"
#include "stubs_generic_layout.h"
#include "stubs_health_layout.h"
#include "stubs_heap.h"
#include "stubs_i18n.h"
#include "stubs_ios_notif_pref_db.h"
#include "stubs_layer.h"
#include "stubs_light.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_menu_cell_layer.h"
#include "stubs_modal_manager.h"
#include "stubs_mutex.h"
#include "stubs_notification_storage.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_peek_layer.h"
#include "stubs_pin_db.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_regular_timer.h"
#include "stubs_reminder_db.h"
#include "stubs_reminders.h"
#include "stubs_serial.h"
#include "stubs_session.h"
#include "stubs_shell_prefs.h"
#include "stubs_simple_dialog.h"
#include "stubs_sleep.h"
#include "stubs_sports_layout.h"
#include "stubs_stringlist.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_time.h"
#include "stubs_timeline.h"
#include "stubs_timeline_actions.h"
#include "stubs_timeline_item.h"
#include "stubs_timeline_layer.h"
#include "stubs_timeline_peek.h"
#include "stubs_vibes.h"
#include "stubs_weather_layout.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
int16_t interpolate_int16(int32_t normalized, int16_t from, int16_t to) {
return to;
}
uint32_t interpolate_uint32(int32_t normalized, uint32_t from, uint32_t to) {
return to;
}
int64_t interpolate_moook(int32_t normalized, int64_t from, int64_t to) {
return to;
}
uint32_t interpolate_moook_duration() {
return 0;
}
int64_t interpolate_moook_soft(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_mid) {
return to;
}
uint32_t interpolate_moook_soft_duration(int32_t num_frames_mid) {
return 0;
}
// Fakes
/////////////////////
#include "fake_animation.h"
#include "fake_app_state.h"
#include "fake_content_indicator.h"
#include "fake_graphics_context.h"
#include "fake_spi_flash.h"
#include "../../fixtures/load_test_resources.h"
typedef struct NotificationWindowTestData {
uint32_t icon_id;
const char *app_name;
const char *title;
const char *subtitle;
const char *location_name;
const char *body;
const char *timestamp;
const char *reminder_timestamp;
GColor primary_color;
GColor background_color;
bool show_notification_timestamp;
bool is_reminder;
struct {
AttributeList attr_list;
TimelineItem timeline_item;
} statics;
} NotificationWindowTestData;
static NotificationWindowTestData s_test_data;
void clock_get_since_time(char *buffer, int buf_size, time_t timestamp) {
if (buffer && s_test_data.timestamp) {
strncpy(buffer, s_test_data.timestamp, (size_t)buf_size);
buffer[buf_size - 1] = '\0';
}
}
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp, int max_relative_hrs) {
if (buffer && s_test_data.reminder_timestamp) {
strncpy(buffer, s_test_data.reminder_timestamp, (size_t)buf_size);
buffer[buf_size - 1] = '\0';
}
}
void clock_copy_time_string(char *buffer, uint8_t buf_size) {
if (buffer) {
strncpy(buffer, "12:00 PM", buf_size);
buffer[buf_size - 1] = '\0';
}
}
//! This function overrides the implementation in swap_layer.c as a way of providing the data
//! we want to display in each notification
LayoutLayer *prv_get_layout_handler(SwapLayer *swap_layer, int8_t rel_position,
void *context) {
// Only support one layout at a time for now
if (rel_position != 0) {
return NULL;
}
NotificationWindowData *data = context;
AttributeList *attr_list = &s_test_data.statics.attr_list;
attribute_list_add_resource_id(attr_list, AttributeIdIconTiny, s_test_data.icon_id);
if (s_test_data.app_name) {
attribute_list_add_cstring(attr_list, AttributeIdAppName, s_test_data.app_name);
}
if (s_test_data.title) {
attribute_list_add_cstring(attr_list, AttributeIdTitle, s_test_data.title);
}
if (s_test_data.subtitle) {
attribute_list_add_cstring(attr_list, AttributeIdSubtitle, s_test_data.subtitle);
}
if (s_test_data.location_name) {
attribute_list_add_cstring(attr_list, AttributeIdLocationName, s_test_data.location_name);
}
if (s_test_data.body) {
attribute_list_add_cstring(attr_list, AttributeIdBody, s_test_data.body);
}
if (!gcolor_is_invisible(s_test_data.primary_color)) {
attribute_list_add_uint8(attr_list, AttributeIdPrimaryColor, s_test_data.primary_color.argb);
}
if (!gcolor_is_invisible(s_test_data.background_color)) {
attribute_list_add_uint8(attr_list, AttributeIdBgColor, s_test_data.background_color.argb);
}
s_test_data.statics.timeline_item = (TimelineItem) {
.header = (CommonTimelineItemHeader) {
.layout = LayoutIdNotification,
.type = s_test_data.is_reminder ? TimelineItemTypeReminder : TimelineItemTypeNotification,
},
.attr_list = *attr_list,
};
TimelineItem *item = &s_test_data.statics.timeline_item;
NotificationLayoutInfo layout_info = (NotificationLayoutInfo) {
.item = item,
.show_notification_timestamp = s_test_data.show_notification_timestamp,
};
const LayoutLayerConfig config = {
.frame = &data->window.layer.bounds,
.attributes = &item->attr_list,
.mode = LayoutLayerModeCard,
.app_id = &data->notification_app_id,
.context = &layout_info,
};
return notification_layout_create(&config);
}
static void prv_property_animation_grect_update(Animation *animation,
const AnimationProgress progress) {
PropertyAnimationPrivate *property_animation = (PropertyAnimationPrivate *)animation;
if (property_animation) {
layer_set_frame(property_animation->subject, &property_animation->values.to.grect);
}
}
static const PropertyAnimationImplementation s_frame_layer_implementation = {
.base.update = prv_property_animation_grect_update,
.accessors = {
.setter.grect = (const GRectSetter)layer_set_frame_by_value,
.getter.grect = (const GRectGetter)layer_get_frame_by_value,
},
};
//! Overrides the stub in stubs_animation.c to provide the proper plumbing for scrolling
PropertyAnimation *property_animation_create_layer_frame(
struct Layer *layer, GRect *from_frame, GRect *to_frame) {
PropertyAnimationPrivate *animation = (PropertyAnimationPrivate *)
property_animation_create(&s_frame_layer_implementation, layer, from_frame, to_frame);
if (from_frame) {
animation->values.from.grect = *from_frame;
PropertyAnimationImplementation *impl =
(PropertyAnimationImplementation *)animation->animation.implementation;
impl->accessors.setter.grect(animation->subject, animation->values.from.grect);
}
if (to_frame) {
animation->values.to.grect = *to_frame;
}
return (PropertyAnimation *)animation;
}
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
// To easily render multiple windows in a single canvas, we'll use an 8-bit bitmap for color
// displays (including round), but we can use the native format for black and white displays (1-bit)
#define CANVAS_GBITMAP_FORMAT PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBITMAP_NATIVE_FORMAT)
// Overrides same function in graphics.c; we need to do this so we can pass in the GBitmapFormat
// we need to use for the unit test output canvas instead of relying on GBITMAP_NATIVE_FORMAT, which
// wouldn't work for Spalding since it uses GBitmapFormat8BitCircular
GBitmap* graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, CANVAS_GBITMAP_FORMAT);
}
// Overrides same function in graphics.c; we need to do this so we can release the framebuffer we're
// using even though its format doesn't match GBITMAP_NATIVE_FORMAT (see comment for mocked
// graphics_capture_frame_buffer() above)
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
void test_notification_window__initialize(void) {
fake_app_state_init();
load_system_resources_fixture();
attribute_list_destroy_list(&s_test_data.statics.attr_list);
s_test_data = (NotificationWindowTestData) {};
}
void test_notification_window__cleanup(void) {
}
// Helpers
//////////////////////
extern NotificationWindowData s_notification_window_data;
//! Static function in swap_layer.c used to scroll
void prv_attempt_scroll(SwapLayer *swap_layer, ScrollDirection direction, bool is_repeating);
static void prv_render_notification_window(unsigned int num_down_scrolls) {
Window *window = &s_notification_window_data.window;
// Set the window on screen so its load/appear handlers will be called
window_set_on_screen(window, true, true);
// Trigger a reload of the NotificationWindow's SwapLayer so it will be updated with the content
// in s_test_data
swap_layer_reload_data(&s_notification_window_data.swap_layer);
// Scroll down the specified number of times
SwapLayer *swap_layer = &s_notification_window_data.swap_layer;
for (int i = 0; i < num_down_scrolls; i++) {
prv_attempt_scroll(swap_layer, ScrollDirectionDown, false /* is_repeating */);
fake_animation_complete(swap_layer->animation);
swap_layer->animation = NULL;
}
// Force the display of the action button
layer_set_hidden(&s_notification_window_data.action_button_layer, false);
// Render the window
window_render(window, fake_graphics_context_get_context());
}
//! @note This must be a multiple of 8 so that we are word-aligned when using a 1-bit bitmap.
#define GRID_CELL_PADDING 8
static void prv_prepare_canvas_and_render_notification_windows(unsigned int num_down_scrolls) {
// Initialize the notification window module before rendering anything
notification_window_init(false /* is_modal */);
const unsigned int num_columns = SettingsContentSizeCount;
const unsigned int num_rows = num_down_scrolls + 1;
const int16_t bitmap_width = (DISP_COLS * num_columns) + (GRID_CELL_PADDING * (num_columns + 1));
const int16_t bitmap_height =
(int16_t)((num_rows == 1) ? DISP_ROWS :
((DISP_ROWS * num_rows) + (GRID_CELL_PADDING * (num_rows + 1))));
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
GBitmap *canvas_bitmap = gbitmap_create_blank(bitmap_size, CANVAS_GBITMAP_FORMAT);
PBL_ASSERTN(canvas_bitmap);
GContext *ctx = fake_graphics_context_get_context();
ctx->dest_bitmap = *canvas_bitmap;
// We modify the bitmap's data pointer below so save a reference to the original here
uint8_t *saved_bitmap_addr = ctx->dest_bitmap.addr;
const uint8_t bitdepth = gbitmap_get_bits_per_pixel(ctx->dest_bitmap.info.format);
// Fill the bitmap with pink (on color) or white (on b&w) so it's easier to see errors
const GColor out_of_bounds_color = PBL_IF_COLOR_ELSE(GColorShockingPink, GColorWhite);
memset(canvas_bitmap->addr, out_of_bounds_color.argb,
canvas_bitmap->row_size_bytes * canvas_bitmap->bounds.size.h);
for (int settings_content_size = 0; settings_content_size < SettingsContentSizeCount;
settings_content_size++) {
const PreferredContentSize content_size =
settings_content_size_to_preferred_size((SettingsContentSize)settings_content_size);
system_theme_set_content_size(content_size);
const int16_t x_offset =
(int16_t)(GRID_CELL_PADDING + (settings_content_size * (GRID_CELL_PADDING + DISP_COLS)));
for (int down_scrolls = 0; down_scrolls <= num_down_scrolls; down_scrolls++) {
const int16_t y_offset =
(int16_t)((num_rows == 1) ? 0 :
GRID_CELL_PADDING + (down_scrolls * (GRID_CELL_PADDING + DISP_ROWS)));
// Set the GContext bitmap's data pointer to the position in the larger bitmap where we
// want to draw this particular notification window
ctx->dest_bitmap.addr =
saved_bitmap_addr + (y_offset * ctx->dest_bitmap.row_size_bytes) +
(x_offset * bitdepth / 8);
prv_render_notification_window((unsigned int)down_scrolls);
// On Round we end up drawing outside the visible screen bounds, so let's draw a circle where
// those bounds are to help us visualize each copy of the screen
#if PBL_ROUND
graphics_context_set_fill_color(ctx, GColorBlack);
graphics_fill_radial(ctx, DISP_FRAME, GOvalScaleModeFitCircle, 1, 0, TRIG_MAX_ANGLE);
#endif
}
}
// Restore the bitmap's original data pointer
ctx->dest_bitmap.addr = saved_bitmap_addr;
}
// Tests
//////////////////////
void test_notification_window__title_body(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_FACEBOOK_MESSENGER,
.title = "Henry Levak",
.body = "Nu, Shara. Where are my designs, blat?",
.show_notification_timestamp = true,
.timestamp = "Just now",
.background_color = GColorPictonBlue,
};
const unsigned int num_down_scrolls =
PBL_IF_RECT_ELSE((PreferredContentSizeDefault < PreferredContentSizeLarge) ? 1 : 0, 0);
prv_prepare_canvas_and_render_notification_windows(num_down_scrolls);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__title_subtitle_body(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_INBOX,
.title = "Henry Levak",
.subtitle = "Henry Levak sent you a 1-1 message",
.body = "Good morning to you my friend!",
.background_color = GColorRed,
};
prv_prepare_canvas_and_render_notification_windows(PBL_IF_RECT_ELSE(2, 1) /* num_down_scrolls */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__reminder(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
.title = "Feed Humphrey",
.location_name = "RWC Office",
.body = "Only the best!",
.reminder_timestamp = "In 15 minutes",
.is_reminder = true,
};
const unsigned int num_down_scrolls =
(PreferredContentSizeDefault >= PreferredContentSizeLarge) ? 0 : 1;
prv_prepare_canvas_and_render_notification_windows(num_down_scrolls);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__body_icon(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_HANGOUTS,
.title = "Kevin Conley",
.subtitle = (PreferredContentSizeDefault >= PreferredContentSizeLarge) ? "New mail!" : NULL,
.body = "",
.background_color = GColorIslamicGreen,
};
prv_prepare_canvas_and_render_notification_windows(0 /* num_down_scrolls */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}