/* * 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 "services/normal/notifications/notification_storage.h" #include "services/normal/notifications/notification_storage_private.h" #include "flash_region/flash_region.h" #include "services/normal/filesystem/pfs.h" #include "util/size.h" #include "clar.h" #include "stdbool.h" // Stubs //////////////////////////////////// #include "fake_spi_flash.h" #include "fake_rtc.h" #include "stubs_analytics.h" #include "stubs_hexdump.h" #include "stubs_layout_layer.h" #include "stubs_logging.h" #include "stubs_mutex.h" #include "stubs_passert.h" #include "stubs_pbl_malloc.h" #include "stubs_pebble_tasks.h" #include "stubs_prompt.h" #include "stubs_rand_ptr.h" #include "stubs_serial.h" #include "stubs_sleep.h" #include "stubs_task_watchdog.h" #define TEST_START FLASH_REGION_FILE_TEST_SPACE_BEGIN #define TEST_SIZE (FLASH_REGION_FILE_TEST_SPACE_END - \ FLASH_REGION_FILE_TEST_SPACE_BEGIN) extern void notification_storage_reset(void); typedef void (*SystemTaskEventCallback)(void *data); static Attribute action1_attributes[] = { {.id = AttributeIdTitle, .cstring = "Dismiss"}, }; static Attribute action2_attributes[] = { {.id = AttributeIdTitle, .cstring = "Archive"}, }; static StringList string_list = { .serialized_byte_length = 3, .data = "A\0B", }; static Attribute action3_attributes[] = { {.id = AttributeIdAncsAction, .int8 = 1}, {.id = AttributeIdCannedResponses, .string_list = &string_list,}, }; static TimelineItemAction actions[] = { { .id = 0, .type = TimelineItemActionTypeResponse, .attr_list = { .num_attributes = ARRAY_LENGTH(action1_attributes), .attributes = action1_attributes } }, { .id = 1, .type = TimelineItemActionTypeResponse, .attr_list = { .num_attributes = ARRAY_LENGTH(action2_attributes), .attributes = action2_attributes } }, { .id = 2, .type = TimelineItemActionTypeResponse, .attr_list = { .num_attributes = ARRAY_LENGTH(action3_attributes), .attributes = action3_attributes } }, }; static Attribute attributes[] = { {.id = AttributeIdTitle, .cstring = "Sender"}, {.id = AttributeIdBody, .cstring = "Message"}, {.id = AttributeIdSubtitle, .cstring = "Subject"}, }; bool system_task_add_callback(SystemTaskEventCallback cb, void *data) { return true; } PebblePhoneCaller* phone_call_util_create_caller(const char *number, const char *name) { return NULL; } // Setup //////////////////////////////////// void test_notification_storage__initialize(void) { fake_spi_flash_init(0, 0x1000000); pfs_init(false); pfs_format(false /* write erase headers */); notification_storage_reset(); } void test_notification_storage__cleanup(void) { } static void compare_attr_list(AttributeList a, AttributeList b) { cl_assert_equal_i(a.num_attributes, b.num_attributes); for (int i = 0; i < a.num_attributes; i++) { cl_assert_equal_i(a.attributes[i].id, b.attributes[i].id); switch (a.attributes[i].id) { case AttributeIdTitle: case AttributeIdSubtitle: case AttributeIdBody: cl_assert_equal_s(a.attributes[i].cstring, b.attributes[i].cstring); break; case AttributeIdAncsAction: cl_assert_equal_i(a.attributes[i].int8, b.attributes[i].int8); break; case AttributeIdCannedResponses: { StringList *list_a = a.attributes[i].string_list; StringList *list_b = b.attributes[i].string_list; cl_assert_equal_i(list_a->serialized_byte_length, list_b->serialized_byte_length); cl_assert_equal_i( string_list_count(list_a), string_list_count(list_b) ); uint32_t count = string_list_count(list_a); for (uint32_t idx = 0; iheader.id, &b->header.id)); cl_assert_equal_i(a->header.ancs_uid, b->header.ancs_uid); cl_assert_equal_i(a->header.status, b->header.status); cl_assert_equal_i(a->header.timestamp, b->header.timestamp); cl_assert_equal_i(a->header.layout, b->header.layout); compare_attr_list(a->attr_list, b->attr_list); cl_assert_equal_i(a->action_group.num_actions, b->action_group.num_actions); for (int i = 0; i < a->action_group.num_actions; i++) { cl_assert_equal_i(a->action_group.actions[i].id, b->action_group.actions[i].id); cl_assert_equal_i(a->action_group.actions[i].type, b->action_group.actions[i].type); compare_attr_list(a->action_group.actions[i].attr_list, b->action_group.actions[i].attr_list); } } // Tests //////////////////////////////////// void test_notification_storage__basic(void) { Uuid id = {0x6b, 0xf6, 0x21, 0x5b, 0xc9, 0x7f, 0x40, 0x9e, 0x8c, 0x31, 0x4f, 0x55, 0x65, 0x72, 0x22, 0xb4}; TimelineItem e = { .header = { .id = id, .status = 0, .layout = LayoutIdGeneric, .type = TimelineItemTypeNotification, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; notification_storage_store(&e); TimelineItem r; cl_assert(notification_storage_get(&id, &r)); compare_notifications(&e, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&id, &r)); compare_notifications(&e, &r); free(r.allocated_buffer); Uuid invalid_uuid; uuid_generate(&invalid_uuid); cl_assert_equal_b(notification_storage_get(&invalid_uuid, &r), false); } void test_notification_storage__multiple(void) { Uuid i1 ; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; Uuid i2; uuid_generate(&i2); TimelineItem e2 = { .header = { .id = i2, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda6, }, .attr_list = { .num_attributes = 2, .attributes = attributes, }, .action_group = { .num_actions = 1, .actions = &actions[2], } }; Uuid i3; uuid_generate(&i3); TimelineItem e3 = { .header = { .id = i3, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda7, }, .attr_list = { .num_attributes = 1, .attributes = &attributes[2], }, .action_group = { .num_actions = 2, .actions = actions, } }; notification_storage_store(&e1); TimelineItem r; cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); notification_storage_store(&e2); notification_storage_store(&e3); cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&i2, &r)); compare_notifications(&e2, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&i3, &r)); compare_notifications(&e3, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); } void test_notification_storage__remove_single(void) { Uuid i; uuid_generate(&i); TimelineItem e = { .header = { .id = i, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; notification_storage_store(&e); TimelineItem r; cl_assert(notification_storage_get(&i, &r)); compare_notifications(&e, &r); free(r.allocated_buffer); notification_storage_remove(&i); cl_assert_equal_b(notification_storage_get(&i, &r), false); } void test_notification_storage__set_actioned_flag(void) { Uuid i; uuid_generate(&i); TimelineItem e = { .header = { .id = i, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; notification_storage_store(&e); TimelineItem r; cl_assert(notification_storage_get(&i, &r)); compare_notifications(&e, &r); free(r.allocated_buffer); notification_storage_set_status(&i, TimelineItemStatusActioned); cl_assert(notification_storage_get(&i, &r)); cl_assert(uuid_equal(&e.header.id, &r.header.id)); cl_assert_equal_i(e.header.ancs_uid, r.header.ancs_uid); cl_assert_equal_i(TimelineItemStatusActioned, r.header.status); cl_assert_equal_i(e.header.timestamp, r.header.timestamp); cl_assert_equal_i(e.header.layout, r.header.layout); compare_attr_list(e.attr_list, r.attr_list); } void test_notification_storage__remove_multiple_first(void) { Uuid i1; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; Uuid i2; uuid_generate(&i2); TimelineItem e2 = { .header = { .id = i2, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda6, }, .attr_list = { .num_attributes = 2, .attributes = attributes, }, .action_group = { .num_actions = 1, .actions = &actions[2], } }; notification_storage_store(&e1); notification_storage_store(&e2); notification_storage_remove(&i1); TimelineItem r; cl_assert_equal_b(notification_storage_get(&i1, &r), false); cl_assert(notification_storage_get(&i2, &r)); compare_notifications(&e2, &r); free(r.allocated_buffer); } void test_notification_storage__remove_add(void) { Uuid i1; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; Uuid i2; uuid_generate(&i2); TimelineItem e2 = { .header = { .id = i2, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda6, }, .attr_list = { .num_attributes = 2, .attributes = attributes, }, .action_group = { .num_actions = 1, .actions = &actions[2], } }; Uuid i3; uuid_generate(&i3); TimelineItem e3 = { .header = { .id = i3, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda7, }, .attr_list = { .num_attributes = 1, .attributes = &attributes[2], }, .action_group = { .num_actions = 2, .actions = actions, } }; notification_storage_store(&e1); notification_storage_store(&e2); notification_storage_remove(&i2); TimelineItem r; cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); cl_assert_equal_b(notification_storage_get(&i2, &r), false); notification_storage_store(&e3); cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); cl_assert_equal_b(notification_storage_get(&i2, &r), false); cl_assert(notification_storage_get(&i3, &r)); compare_notifications(&e3, &r); free(r.allocated_buffer); e2.header.timestamp = e3.header.timestamp + 1; notification_storage_store(&e2); cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&e1, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&i2, &r)); compare_notifications(&e2, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&i3, &r)); compare_notifications(&e3, &r); free(r.allocated_buffer); } void test_notification_storage__remove_add_compress(void) { time_t timestamp = 0x10000000; TimelineItem e = { .header = { .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; const size_t notif_size = sizeof(SerializedTimelineItemHeader) + timeline_item_get_serialized_payload_size(&e); const size_t file_size = NOTIFICATION_STORAGE_FILE_SIZE; const size_t count = file_size / notif_size; Uuid uuids[count]; int i; for (i = 0; i < count; i++) { uuid_generate(&uuids[i]); e.header.id = uuids[i]; e.header.timestamp = timestamp + i; notification_storage_store(&e); } TimelineItem r; cl_assert(notification_storage_get(&uuids[0], &r)); e.header.id = uuids[0]; e.header.timestamp = timestamp; compare_notifications(&e, &r); free(r.allocated_buffer); cl_assert(notification_storage_get(&uuids[i - 1], &r)); e.header.id = uuids[i - 1]; e.header.timestamp = timestamp + i - 1; compare_notifications(&e, &r); free(r.allocated_buffer); // Storage is now full and none have been removed. // As more space is needed, blocks of 4k are freed up Uuid uuid; uuid_generate(&uuid); e.header.id = uuid; e.header.timestamp = timestamp + i; notification_storage_store(&e); cl_assert(notification_storage_get(&uuid, &r)); compare_notifications(&e, &r); free(r.allocated_buffer); const size_t block_size = file_size / 4; int erase_count = block_size / notif_size + (((block_size % notif_size) > 0) ? 1 : 0); int j; for (j = 0; j < erase_count; j++) { e.header.id = uuids[j]; e.header.timestamp = timestamp + j; cl_assert_equal_b(notification_storage_get(&uuids[j], &r), false); } e.header.id = uuids[j]; e.header.timestamp = timestamp + j; cl_assert(notification_storage_get(&uuids[j], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); // Fill up storage again for (++i; i < count + erase_count; i++) { e.header.id = uuids[i % count]; e.header.timestamp = timestamp + i; notification_storage_store(&e); cl_assert(notification_storage_get(&uuids[i % count], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); } //Check that no notifications have been deleted e.header.id = uuids[j]; e.header.timestamp = timestamp + j; cl_assert(notification_storage_get(&uuids[j], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); //Free up enough space for one notification by removing one notification_storage_remove(&uuids[i/2]); e.header.id = uuids[i/2]; e.header.timestamp = timestamp + i/2; cl_assert_equal_b(notification_storage_get(&uuids[i/2], &r), false); //Add another notification. Compression should take place without freeing up a 4k block e.header.id = uuids[i]; e.header.timestamp = timestamp + i; notification_storage_store(&e); cl_assert(notification_storage_get(&uuids[i], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); //Ensure that compression does not remove old notifications e.header.id = uuids[j]; e.header.timestamp = timestamp + j; cl_assert(notification_storage_get(&uuids[j], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); //Add another notification. Compression should free up another 4k block e.header.id = uuids[i]; e.header.timestamp = timestamp + i; notification_storage_store(&e); cl_assert(notification_storage_get(&uuids[i], &r)); compare_notifications(&e, &r); free(r.allocated_buffer); // Check that an expected number of notifications were removed erase_count += j; for (; j < erase_count; j++) { e.header.id = uuids[j]; e.header.timestamp = timestamp + j; cl_assert_equal_b(notification_storage_get(&uuids[j], &r), false); } } void test_notification_storage__find_ancs_id(void) { Uuid i1; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; Uuid i2; uuid_generate(&i2); TimelineItem e2 = { .header = { .id = i2, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 1, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda6, }, .attr_list = { .num_attributes = 2, .attributes = attributes, }, .action_group = { .num_actions = 1, .actions = &actions[2], } }; Uuid i3; uuid_generate(&i3); TimelineItem e3 = { .header = { .id = i3, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 84, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda7, }, .attr_list = { .num_attributes = 1, .attributes = &attributes[2], }, .action_group = { .num_actions = 2, .actions = actions, } }; notification_storage_store(&e1); notification_storage_store(&e2); notification_storage_store(&e3); Uuid u; cl_assert(notification_storage_find_ancs_notification_id(0, &u)); cl_assert(uuid_equal(&u, &e1.header.id)); cl_assert(notification_storage_find_ancs_notification_id(1, &u)); cl_assert(uuid_equal(&u, &e2.header.id)); cl_assert(notification_storage_find_ancs_notification_id(84, &u)); cl_assert(uuid_equal(&u, &e3.header.id)); // Add a new notification with the same ANCS UID as an existing notification to make sure we // return the new notification instead of the old one TimelineItem e4 = e3; uuid_generate(&e4.header.id); notification_storage_store(&e4); cl_assert(notification_storage_find_ancs_notification_id(84, &u)); cl_assert(uuid_equal(&u, &e4.header.id)); } void test_notification_storage__find_by_timestamp(void) { Uuid i1; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; Uuid i2; uuid_generate(&i2); TimelineItem e2 = { .header = { .id = i2, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 1, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda6, }, .attr_list = { .num_attributes = 2, .attributes = attributes, }, .action_group = { .num_actions = 1, .actions = &actions[2], } }; Uuid i3; uuid_generate(&i3); TimelineItem e3 = { .header = { .id = i3, .type = TimelineItemTypeNotification, .status = 0, .ancs_uid = 84, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda7, }, .attr_list = { .num_attributes = 1, .attributes = &attributes[2], }, .action_group = { .num_actions = 2, .actions = actions, } }; notification_storage_store(&e1); notification_storage_store(&e2); notification_storage_store(&e3); TimelineItem test = e1; test.header.id = UUID_INVALID; test.header.ancs_uid = 51; CommonTimelineItemHeader h; cl_assert(notification_storage_find_ancs_notification_by_timestamp(&test, &h)); cl_assert_equal_m(&e1.header, &h, sizeof(CommonTimelineItemHeader)); test.action_group.num_actions = 2; cl_assert_equal_b(notification_storage_find_ancs_notification_by_timestamp(&test, &h), false); test = e2; test.header.id = UUID_INVALID; cl_assert(notification_storage_find_ancs_notification_by_timestamp(&test, &h)); cl_assert_equal_m(&e2.header, &h, sizeof(CommonTimelineItemHeader)); test = e3; test.header.id = UUID_INVALID; cl_assert(notification_storage_find_ancs_notification_by_timestamp(&test, &h)); cl_assert_equal_m(&e3.header, &h, sizeof(CommonTimelineItemHeader)); notification_storage_remove(&i2); test = e2; test.header.id = UUID_INVALID; cl_assert_equal_b(notification_storage_find_ancs_notification_by_timestamp(&test, &h), false); test = e3; test.header.id = UUID_INVALID; cl_assert(notification_storage_find_ancs_notification_by_timestamp(&test, &h)); cl_assert_equal_m(&e3.header, &h, sizeof(CommonTimelineItemHeader)); } void test_notification_storage__should_detect_corruption(void) { Uuid i1; uuid_generate(&i1); TimelineItem e1 = { .header = { .id = i1, .type = TimelineItemTypeNotification, .status = TimelineItemStatusRead, .ancs_uid = 0, .layout = LayoutIdGeneric, .timestamp = 0x53f0dda5, }, .attr_list = { .num_attributes = ARRAY_LENGTH(attributes), .attributes = attributes, }, .action_group = { .num_actions = ARRAY_LENGTH(actions), .actions = actions, } }; notification_storage_store(&e1); TimelineItem r; cl_assert(notification_storage_get(&i1, &r)); compare_notifications(&r, &e1); TimelineItem e2 = e1; Uuid i2; uuid_generate(&i2); e2.header.id = i2; e2.header.status = 0xC0; notification_storage_store(&e2); cl_assert_equal_b(notification_storage_get(&i2, &r), false); TimelineItem e3 = e1; Uuid i3; uuid_generate(&i3); e3.header.id = i3; e3.header.type = TimelineItemTypeOutOfRange; notification_storage_store(&e3); cl_assert_equal_b(notification_storage_get(&i3, &r), false); Uuid i4; uuid_generate(&i4); TimelineItem e4 = e1; e4.header.id = i4; e4.header.layout = NumLayoutIds; notification_storage_store(&e4); cl_assert_equal_b(notification_storage_get(&i4, &r), false); }