mirror of
https://github.com/google/pebble.git
synced 2025-03-21 11:21:21 +00:00
461 lines
16 KiB
C
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();
|
|
}
|