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