mirror of
https://github.com/google/pebble.git
synced 2025-03-20 02:51:21 +00:00
462 lines
16 KiB
C
462 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 "applib/graphics/bitblt.h"
|
|
#include "applib/graphics/framebuffer.h"
|
|
#include "applib/graphics/graphics.h"
|
|
#include "applib/ui/app_window_stack.h"
|
|
#include "applib/ui/text_layer.h"
|
|
#include "applib/ui/window_private.h"
|
|
#include "popups/timeline/peek_private.h"
|
|
#include "resource/resource.h"
|
|
#include "resource/resource_ids.auto.h"
|
|
#include "services/normal/timeline/timeline_resources.h"
|
|
#include "util/buffer.h"
|
|
#include "util/graphics.h"
|
|
#include "util/hash.h"
|
|
#include "util/math.h"
|
|
#include "util/size.h"
|
|
#include "util/trig.h"
|
|
|
|
#include "clar.h"
|
|
|
|
#include <stdio.h>
|
|
|
|
// Fakes
|
|
/////////////////////
|
|
|
|
#include "fake_rtc.h"
|
|
#include "fake_spi_flash.h"
|
|
#include "fixtures/load_test_resources.h"
|
|
|
|
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp, int max_relative_hrs) {
|
|
snprintf(buffer, buf_size, "In 5 minutes");
|
|
}
|
|
|
|
// Stubs
|
|
/////////////////////
|
|
|
|
#include "stubs_activity.h"
|
|
#include "stubs_analytics.h"
|
|
#include "stubs_animation_timing.h"
|
|
#include "stubs_app_install_manager.h"
|
|
#include "stubs_app_state.h"
|
|
#include "stubs_app_timer.h"
|
|
#include "stubs_bootbits.h"
|
|
#include "stubs_click.h"
|
|
#include "stubs_cron.h"
|
|
#include "stubs_event_loop.h"
|
|
#include "stubs_i18n.h"
|
|
#include "stubs_layer.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_passert.h"
|
|
#include "stubs_pbl_malloc.h"
|
|
#include "stubs_pebble_process_info.h"
|
|
#include "stubs_pebble_tasks.h"
|
|
#include "stubs_pin_db.h"
|
|
#include "stubs_process_manager.h"
|
|
#include "stubs_prompt.h"
|
|
#include "stubs_property_animation.h"
|
|
#include "stubs_scroll_layer.h"
|
|
#include "stubs_serial.h"
|
|
#include "stubs_shell_prefs.h"
|
|
#include "stubs_sleep.h"
|
|
#include "stubs_status_bar_layer.h"
|
|
#include "stubs_syscalls.h"
|
|
#include "stubs_task_watchdog.h"
|
|
#include "stubs_timeline_event.h"
|
|
#include "stubs_timeline_layer.h"
|
|
#include "stubs_unobstructed_area.h"
|
|
#include "stubs_window_manager.h"
|
|
#include "stubs_window_stack.h"
|
|
|
|
// Helper Functions
|
|
/////////////////////
|
|
|
|
#include "fw/graphics/test_graphics.h"
|
|
#include "fw/graphics/util.h"
|
|
|
|
static GContext s_ctx;
|
|
|
|
GContext *graphics_context_get_current_context(void) {
|
|
return &s_ctx;
|
|
}
|
|
|
|
static bool s_is_watchface_running;
|
|
|
|
bool app_manager_is_watchface_running(void) {
|
|
return s_is_watchface_running;
|
|
}
|
|
|
|
// Setup and Teardown
|
|
////////////////////////////////////
|
|
|
|
static FrameBuffer s_fb;
|
|
|
|
static GBitmap *s_dest_bitmap;
|
|
|
|
void test_timeline_peek__initialize(void) {
|
|
// Setup time
|
|
TimezoneInfo tz_info = {
|
|
.tm_zone = "UTC",
|
|
};
|
|
time_util_update_timezone(&tz_info);
|
|
rtc_set_timezone(&tz_info);
|
|
rtc_set_time(SECONDS_PER_DAY);
|
|
|
|
// We start time out at 5pm on Jan 1, 2015 for all of these tests
|
|
struct tm time_tm = {
|
|
// Thursday, Jan 1, 2015, 5pm
|
|
.tm_hour = 17,
|
|
.tm_mday = 1,
|
|
.tm_year = 115
|
|
};
|
|
|
|
const time_t utc_sec = mktime(&time_tm);
|
|
fake_rtc_init(0 /* initial_ticks */, utc_sec);
|
|
|
|
// Setup graphics context
|
|
framebuffer_init(&s_fb, &DISP_FRAME.size);
|
|
framebuffer_clear(&s_fb);
|
|
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
|
|
s_app_state_get_graphics_context = &s_ctx;
|
|
|
|
// Setup resources
|
|
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
|
|
pfs_init(false /* run filesystem check */);
|
|
pfs_format(true /* write erase headers */);
|
|
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
|
|
false /* is_next */);
|
|
resource_init();
|
|
|
|
// Initialize peek
|
|
s_is_watchface_running = true;
|
|
timeline_peek_init();
|
|
}
|
|
|
|
void test_timeline_peek__cleanup(void) {
|
|
}
|
|
|
|
// Helpers
|
|
//////////////////////
|
|
|
|
static void prv_render_layer(Layer *layer, const GRect *box, bool use_screen) {
|
|
gbitmap_destroy(s_dest_bitmap);
|
|
|
|
const GRect *drawing_box = use_screen ? &DISP_FRAME : box;
|
|
const GSize bitmap_size = drawing_box->size;
|
|
s_dest_bitmap = gbitmap_create_blank(bitmap_size, GBITMAP_NATIVE_FORMAT);
|
|
|
|
s_ctx.dest_bitmap = *s_dest_bitmap;
|
|
s_ctx.draw_state.clip_box.size = bitmap_size;
|
|
s_ctx.draw_state.drawing_box = *drawing_box;
|
|
|
|
layer_render_tree(layer, &s_ctx);
|
|
|
|
if (use_screen) {
|
|
GBitmap *screen_bitmap = s_dest_bitmap;
|
|
screen_bitmap->bounds = (GRect) { gpoint_neg(box->origin), box->size };
|
|
s_dest_bitmap = gbitmap_create_blank(box->size, PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBitmapFormat1Bit));
|
|
bitblt_bitmap_into_bitmap(s_dest_bitmap, screen_bitmap, GPointZero, GCompOpAssign,
|
|
GColorClear);
|
|
gbitmap_destroy(screen_bitmap);
|
|
}
|
|
}
|
|
|
|
typedef struct TimelinePeekItemConfig {
|
|
time_t timestamp;
|
|
const char *title;
|
|
const char *subtitle;
|
|
TimelineResourceId icon;
|
|
int num_concurrent;
|
|
} TimelinePeekItemConfig;
|
|
|
|
static TimelineItem *prv_set_timeline_item(const TimelinePeekItemConfig *config, bool animated) {
|
|
TimelineItem *item = NULL;
|
|
const time_t now = rtc_get_time();
|
|
const time_t timestamp = config ? (config->timestamp ?: now) : now;
|
|
if (config) {
|
|
AttributeList list;
|
|
attribute_list_init_list(3 /* num_attributes */, &list);
|
|
attribute_list_add_cstring(&list, AttributeIdTitle, config->title);
|
|
if (config->subtitle) {
|
|
attribute_list_add_cstring(&list, AttributeIdSubtitle, config->subtitle);
|
|
}
|
|
attribute_list_add_uint32(&list, AttributeIdIconPin, config->icon);
|
|
item = timeline_item_create_with_attributes(timestamp, MINUTES_PER_HOUR,
|
|
TimelineItemTypePin, LayoutIdGeneric,
|
|
&list, NULL);
|
|
attribute_list_destroy_list(&list);
|
|
}
|
|
timeline_peek_set_item(item, timestamp >= now, config ? config->num_concurrent : 0,
|
|
false /* first */, animated);
|
|
return item;
|
|
}
|
|
|
|
static void prv_render_timeline_peek(const TimelinePeekItemConfig *config) {
|
|
TimelineItem *item = prv_set_timeline_item(config, false /* animated */);
|
|
// Force timeline peek to be visible
|
|
timeline_peek_set_visible(true, false /* animated */);
|
|
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// For text flow, the whole screen is needed. Render the screen, then reduce to the layer.
|
|
const bool use_screen = PBL_IF_ROUND_ELSE(true, false);
|
|
prv_render_layer(&peek->window.layer,
|
|
&(GRect) { gpoint_neg(layer->frame.origin), layer->frame.size },
|
|
use_screen);
|
|
|
|
timeline_item_destroy(item);
|
|
}
|
|
|
|
// Visual Layout Tests
|
|
//////////////////////
|
|
|
|
void test_timeline_peek__peek(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "CoreUX Design x Eng",
|
|
.subtitle = "ConfRM-Missile Command",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
|
|
.num_concurrent = 0,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_newline(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "NY 3\nSF 12",
|
|
.subtitle = "Bottom of\nthe 9th",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
|
|
.num_concurrent = 1,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_title_only_newline(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "NY 3\nSF 12",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
|
|
.num_concurrent = 1,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_concurrent_1(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "NY 3 - SF 12",
|
|
.subtitle = "Bottom of the 9th",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
|
|
.num_concurrent = 1,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_concurrent_2(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "Stock for party 🍺",
|
|
.subtitle = "Pebble Pad on Park",
|
|
.icon = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
|
|
.num_concurrent = 2,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_concurrent_2_max(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = ":parrot: :parrot:",
|
|
.subtitle = ":parrot: :parrot: :parrot:",
|
|
.icon = TIMELINE_RESOURCE_GENERIC_CONFIRMATION,
|
|
.num_concurrent = 3,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_title_only(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "Trash up the Place 🔥",
|
|
.icon = TIMELINE_RESOURCE_TIDE_IS_HIGH,
|
|
.num_concurrent = 0,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_title_only_concurrent_1(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "No Watch No Life",
|
|
.icon = TIMELINE_RESOURCE_DAY_SEPARATOR,
|
|
.num_concurrent = 1,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_title_only_concurrent_2(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.title = "OMG I think the text fits!",
|
|
.icon = TIMELINE_RESOURCE_GENERIC_WARNING,
|
|
.num_concurrent = 2,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
void test_timeline_peek__peek_in_5_minutes(void) {
|
|
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
|
|
.timestamp = rtc_get_time() + (5 * SECONDS_PER_MINUTE),
|
|
.title = "Stock for party 🍺",
|
|
.subtitle = "Pebble Pad on Park",
|
|
.icon = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
|
|
.num_concurrent = 2,
|
|
});
|
|
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
|
|
}
|
|
|
|
// Visibility Tests
|
|
//////////////////////
|
|
|
|
void test_timeline_peek__peek_visibility(void) {
|
|
prv_set_timeline_item(NULL, false /* animated */);
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// Normally it is animated, but for this unit test, we don't request `animated`
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Peek service shows the peek UI. Not animated for this unit test.
|
|
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
|
|
.title = "CoreUX Design x Eng",
|
|
.subtitle = "ConfRM-Missile Command",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
|
|
.num_concurrent = 0,
|
|
}, false /* animated */);
|
|
// Peek should now be on-screen.
|
|
cl_assert(layer->frame.origin.y < DISP_ROWS);
|
|
|
|
// Peek service hides the peek UI. Not animated for this unit test.
|
|
prv_set_timeline_item(NULL, false /* animated */);
|
|
// Peek should now be off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
}
|
|
|
|
void test_timeline_peek__peek_visible_to_hidden_outside_of_watchface(void) {
|
|
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
|
|
.title = "CoreUX Design x Eng",
|
|
.subtitle = "ConfRM-Missile Command",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
|
|
.num_concurrent = 0,
|
|
}, false /* animated */);
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// Normally it is animated, but for this unit test, we don't request `animated`
|
|
cl_assert(layer->frame.origin.y < DISP_ROWS);
|
|
|
|
// Transition away from the watchface
|
|
s_is_watchface_running = false;
|
|
timeline_peek_set_visible(false, false /* animated */);
|
|
// For simplicity, the implementation also moves the layer even though it is not necessary.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Peek service hides the peek UI using the animated code path.
|
|
prv_set_timeline_item(NULL, true /* animated */);
|
|
// This time we set the item to NULL, not just request invisibility. Since we're not in the
|
|
// watchface, even though `animated` was requested, it should immediately move the position.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition back to the watchface
|
|
s_is_watchface_running = true;
|
|
timeline_peek_set_visible(true, false /* animated */);
|
|
// Peek should be visible again, but it should still be off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
timeline_item_destroy(item);
|
|
}
|
|
|
|
void test_timeline_peek__peek_hidden_to_visible_outside_of_watchface(void) {
|
|
prv_set_timeline_item(NULL, false /* animated */);
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// Normally it is animated, but for this unit test, we don't request `animated`
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition away from the watchface
|
|
s_is_watchface_running = false;
|
|
timeline_peek_set_visible(false, false /* animated */);
|
|
// For simplicity, the implementation also moves the layer even though it is not necessary.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Peek service shows the peek UI using the animated code path.
|
|
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
|
|
.title = "CoreUX Design x Eng",
|
|
.subtitle = "ConfRM-Missile Command",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
|
|
.num_concurrent = 0,
|
|
}, true /* animated */);
|
|
// Since we're not in the watchface, the peek remains off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition back to the watchface
|
|
s_is_watchface_running = true;
|
|
timeline_peek_set_visible(true, false /* animated */);
|
|
// Peek should be visible again and now on-screen.
|
|
cl_assert(layer->frame.origin.y < DISP_ROWS);
|
|
timeline_item_destroy(item);
|
|
}
|
|
|
|
void test_timeline_peek__peek_visible_leaving_and_entering_watchface(void) {
|
|
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
|
|
.title = "CoreUX Design x Eng",
|
|
.subtitle = "ConfRM-Missile Command",
|
|
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
|
|
.num_concurrent = 0,
|
|
}, false /* animated */);
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// Normally it is animated, but for this unit test, we don't request `animated`
|
|
cl_assert(layer->frame.origin.y < DISP_ROWS);
|
|
|
|
// Transition away from the watchface
|
|
s_is_watchface_running = false;
|
|
timeline_peek_set_visible(false, false /* animated */);
|
|
// For simplicity, the implementation also moves the layer even though it is not necessary.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition back to the watchface
|
|
s_is_watchface_running = true;
|
|
timeline_peek_set_visible(true, false /* animated */);
|
|
// Peek should be visible again and on-screen.
|
|
cl_assert(layer->frame.origin.y < DISP_ROWS);
|
|
timeline_item_destroy(item);
|
|
}
|
|
|
|
void test_timeline_peek__peek_hidden_leaving_and_entering_watchface(void) {
|
|
prv_set_timeline_item(NULL, true /* animated */);
|
|
TimelinePeek *peek = timeline_peek_get_peek();
|
|
const Layer *layer = &peek->layout_layer;
|
|
// Peek should be off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition away from the watchface
|
|
s_is_watchface_running = false;
|
|
timeline_peek_set_visible(false, false /* animated */);
|
|
// Peek should be hidden and off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
|
|
// Transition back to the watchface
|
|
s_is_watchface_running = true;
|
|
timeline_peek_set_visible(true, false /* animated */);
|
|
// Peek should be visible again, but it should still be off-screen.
|
|
cl_assert(layer->frame.origin.y >= DISP_ROWS);
|
|
}
|