mirror of
https://github.com/google/pebble.git
synced 2025-03-19 02:21:21 +00:00
720 lines
25 KiB
C
720 lines
25 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 "comm/ble/kernel_le_client/ams/ams.h"
|
|
|
|
#include "comm/bt_conn_mgr.h"
|
|
|
|
#include "services/normal/music.h"
|
|
#include "services/normal/music_internal.h"
|
|
|
|
#include "clar.h"
|
|
|
|
// Stubs & Fakes
|
|
///////////////////////////////////////////////////////////
|
|
|
|
#include "fake_events.h"
|
|
#include "fake_gatt_client_operations.h"
|
|
#include "fake_gatt_client_subscriptions.h"
|
|
#include "fake_pebble_tasks.h"
|
|
#include "fake_rtc.h"
|
|
|
|
#include "stubs_app_install_manager.h"
|
|
#include "stubs_app_manager.h"
|
|
#include "stubs_bt_lock.h"
|
|
#include "stubs_hexdump.h"
|
|
#include "stubs_logging.h"
|
|
#include "stubs_mutex.h"
|
|
#include "stubs_passert.h"
|
|
#include "stubs_pbl_malloc.h"
|
|
#include "stubs_serial.h"
|
|
#include "stubs_tick.h"
|
|
|
|
void analytics_event_ams(uint8_t type, int32_t aux_info) {
|
|
}
|
|
|
|
struct {
|
|
ResponseTimeState state;
|
|
uint16_t max_period_secs;
|
|
} s_conn_mgr_states[NumBtConsumer];
|
|
|
|
void conn_mgr_set_ble_conn_response_time(GAPLEConnection *hdl, BtConsumer consumer,
|
|
ResponseTimeState state, uint16_t max_period_secs) {
|
|
s_conn_mgr_states[consumer].state = state;
|
|
s_conn_mgr_states[consumer].max_period_secs = max_period_secs;
|
|
}
|
|
|
|
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
|
|
return NULL;
|
|
}
|
|
|
|
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
|
|
return (BTDeviceInternal) {
|
|
.address.octets = {
|
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
|
|
},
|
|
};
|
|
}
|
|
|
|
static void (*s_launcher_task_callback)(void *data);
|
|
static void *s_launcher_task_callback_data;
|
|
|
|
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
|
|
// Simple fake, can only handle one call
|
|
cl_assert_equal_p(s_launcher_task_callback, NULL);
|
|
|
|
s_launcher_task_callback = callback;
|
|
s_launcher_task_callback_data = data;
|
|
}
|
|
|
|
// Tests: Disover AMS
|
|
///////////////////////////////////////////////////////////
|
|
#define NUM_AMS_INSTANCES 2
|
|
static BLECharacteristic s_characteristics[NUM_AMS_INSTANCES][NumAMSCharacteristic] = {
|
|
// AMS instance one:
|
|
[0] = {
|
|
[AMSCharacteristicRemoteCommand] = 1,
|
|
[AMSCharacteristicEntityUpdate] = 2,
|
|
[AMSCharacteristicEntityAttribute] = 3,
|
|
},
|
|
// AMS instance two:
|
|
[1] = {
|
|
[AMSCharacteristicRemoteCommand] = 4,
|
|
[AMSCharacteristicEntityUpdate] = 5,
|
|
[AMSCharacteristicEntityAttribute] = 6,
|
|
},
|
|
};
|
|
|
|
static const BLECharacteristic s_unknown_characteristic = 999;
|
|
|
|
static void prv_assert_can_handle_characteristics(uint32_t instance_idx, bool expect_can_handle) {
|
|
for (AMSCharacteristic c = 0; c < NumAMSCharacteristic; ++c) {
|
|
cl_assert_equal_b(ams_can_handle_characteristic(s_characteristics[instance_idx][c]),
|
|
expect_can_handle);
|
|
}
|
|
}
|
|
|
|
static void prv_discover_ams(uint8_t num_instances) {
|
|
cl_assert(num_instances <= 2);
|
|
if (num_instances) {
|
|
for (int i = 0; i < num_instances && i < NUM_AMS_INSTANCES; i++) {
|
|
ams_handle_service_discovered(&s_characteristics[i][0]);
|
|
}
|
|
} else {
|
|
ams_invalidate_all_references();
|
|
}
|
|
}
|
|
|
|
void test_ams__cannot_handle_any_characteristic_after_destroy(void) {
|
|
prv_discover_ams(1);
|
|
ams_destroy();
|
|
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
|
|
}
|
|
|
|
void test_ams__cannot_handle_unknown_characteristic(void) {
|
|
prv_discover_ams(1);
|
|
cl_assert_equal_b(ams_can_handle_characteristic(s_unknown_characteristic), false);
|
|
}
|
|
|
|
void test_ams__discover_of_ams_should_subscribe_to_entity_update_characteristic(void) {
|
|
// Pass in 2 instances, it should be able to cope with this
|
|
prv_discover_ams(2 /* num_instances */);
|
|
|
|
// Assert ams.c can now handle the characteristic reference for the first instance:
|
|
prv_assert_can_handle_characteristics(0, true /* expect_can_handle */);
|
|
|
|
// Assert ams.c can not handle the characteristic reference for the second instance:
|
|
prv_assert_can_handle_characteristics(1, false /* expect_can_handle */);
|
|
|
|
// The first instance is expected to be used.
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
fake_gatt_client_subscriptions_assert_subscribe(entity_update,
|
|
BLESubscriptionNotifications, GAPLEClientKernel);
|
|
}
|
|
|
|
void test_ams__connect_to_music_service_upon_subscribing_entity_update_characteristic(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
// Not connected yet (still need to subscribe):
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
// Music service should be connected now:
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), ams_music_server_debug_name());
|
|
|
|
// Rediscovery will disconnect music service (until resubscribed):
|
|
ams_invalidate_all_references();
|
|
prv_discover_ams(1 /* num_instances */);
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
}
|
|
|
|
void test_ams__dont_connect_music_service_if_subscribe_entity_update_characteristic_fails(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
|
|
// Simulate failed subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorUnlikelyError);
|
|
|
|
// Not connected because subscription failed:
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
}
|
|
|
|
void test_ams__update_characteristics_ams_not_found(void) {
|
|
// AMS Found:
|
|
prv_discover_ams(1 /* num_instances */);
|
|
// AMS Disappeared:
|
|
prv_discover_ams(0 /* num_instances */);
|
|
// Verify ams.c cannot handle the previous characteristics any more:
|
|
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
|
|
}
|
|
|
|
// Tests: Register for Entity Updates
|
|
///////////////////////////////////////////////////////////
|
|
|
|
static const uint8_t s_register_player_entity[] = {0x00,
|
|
// Apple bug #21283910
|
|
// See ams.c, prv_get_registration_cmd_for_entity
|
|
// 0x00,
|
|
0x01, 0x02};
|
|
|
|
static const uint8_t s_register_queue_entity[] = {0x01, 0x00, 0x01, 0x02, 0x03};
|
|
|
|
static const uint8_t s_register_track_entity[] = {0x02, 0x00, 0x01, 0x02, 0x03};
|
|
|
|
void test_ams__register_for_entity_updates(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
// Expect to have written the command to register for the Player entity:
|
|
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
|
|
sizeof(s_register_player_entity), GAPLEClientKernel,
|
|
true /* is_response_required */);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
|
|
|
|
// Expect to have written the command to register for the Queue entity:
|
|
fake_gatt_client_op_assert_write(entity_update, s_register_queue_entity,
|
|
sizeof(s_register_queue_entity), GAPLEClientKernel,
|
|
true /* is_response_required */);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
|
|
|
|
// Expect to have written the command to register for the Track entity:
|
|
fake_gatt_client_op_assert_write(entity_update, s_register_track_entity,
|
|
sizeof(s_register_track_entity), GAPLEClientKernel,
|
|
true /* is_response_required */);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
|
|
|
|
// After GATT re-discovery, the registration needs to happen again
|
|
ams_invalidate_all_references();
|
|
prv_discover_ams(1 /* num_instances */);
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
|
|
}
|
|
|
|
void test_ams__register_for_entity_updates_retry_if_out_of_resources(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
|
|
// Simulate not having enough resources to process the request:
|
|
fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
// Nothing should have been written because out of resources:
|
|
fake_gatt_client_op_assert_no_write();
|
|
|
|
// Resources come available again:
|
|
fake_gatt_client_op_set_write_return_value(BTErrnoOK);
|
|
|
|
// Simulate processing the callback to retry:
|
|
cl_assert(s_launcher_task_callback != NULL);
|
|
s_launcher_task_callback(s_launcher_task_callback_data);
|
|
|
|
// Expect to have written the command to register for the Player entity:
|
|
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
|
|
sizeof(s_register_player_entity), GAPLEClientKernel,
|
|
true /* is_response_required */);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
}
|
|
|
|
static const MusicServerImplementation s_dummy_server_implementation = {};
|
|
static void prv_set_dummy_server_connected(bool connected) {
|
|
music_set_connected_server(&s_dummy_server_implementation,
|
|
connected /* connected */);
|
|
}
|
|
|
|
void test_ams__dont_register_if_another_music_server_is_already_connected(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
// Not connected yet (still need to subscribe):
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
|
|
prv_set_dummy_server_connected(true /* connected */);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
// Nothing should have been written, because there's already a music server connected.
|
|
fake_gatt_client_op_assert_no_write();
|
|
|
|
// Clean up after ourselves:
|
|
prv_set_dummy_server_connected(false /* connected */);
|
|
}
|
|
|
|
// Tests: Sending Remote Commands
|
|
///////////////////////////////////////////////////////////
|
|
|
|
static void prv_connect_ams(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
// Simulate successful write responses (for the Entity Update registration write requests):
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
|
|
|
|
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
|
|
|
|
fake_gatt_client_op_clear_write_list();
|
|
}
|
|
|
|
static uint8_t prv_ams_command_for_music_command(MusicCommand command) {
|
|
switch (command) {
|
|
case MusicCommandPlay:
|
|
return 0x00;
|
|
case MusicCommandPause:
|
|
return 0x01;
|
|
case MusicCommandTogglePlayPause:
|
|
return 0x02;
|
|
case MusicCommandNextTrack:
|
|
return 0x03;
|
|
case MusicCommandPreviousTrack:
|
|
return 0x04;
|
|
case MusicCommandVolumeUp:
|
|
return 0x05;
|
|
case MusicCommandVolumeDown:
|
|
return 0x06;
|
|
case MusicCommandAdvanceRepeatMode:
|
|
return 0x07;
|
|
case MusicCommandAdvanceShuffleMode:
|
|
return 0x08;
|
|
case MusicCommandSkipForward:
|
|
return 0x09;
|
|
case MusicCommandSkipBackward:
|
|
return 0x0A;
|
|
case MusicCommandLike:
|
|
return 0x0B;
|
|
case MusicCommandDislike:
|
|
return 0x0C;
|
|
case MusicCommandBookmark:
|
|
return 0x0D;
|
|
default:
|
|
cl_assert(false);
|
|
return 0xFF;
|
|
}
|
|
}
|
|
|
|
void test_ams__send_remote_command(void) {
|
|
prv_connect_ams();
|
|
|
|
// Exercise all MusicCommand types (currently they are all supported):
|
|
for (MusicCommand music_cmd = 0; music_cmd < NumMusicCommand; ++music_cmd) {
|
|
music_command_send(music_cmd);
|
|
|
|
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
|
|
const uint8_t ams_cmd = prv_ams_command_for_music_command(music_cmd);
|
|
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
|
|
GAPLEClientKernel, true /* is_response_required */);
|
|
|
|
// Simulate receiving the response:
|
|
ams_handle_write_response(remote_command, BLEGATTErrorSuccess);
|
|
}
|
|
|
|
// Invalid/Unsupported command:
|
|
music_command_send(NumMusicCommand);
|
|
fake_gatt_client_op_assert_no_write();
|
|
}
|
|
|
|
void test_ams__send_remote_command_non_kernel_main_task(void) {
|
|
prv_connect_ams();
|
|
|
|
// Simulate Music app calling music_command_send():
|
|
stub_pebble_tasks_set_current(PebbleTask_App);
|
|
music_command_send(MusicCommandPlay);
|
|
|
|
// Process the KernelMain callback:
|
|
cl_assert(s_launcher_task_callback != NULL);
|
|
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
|
|
s_launcher_task_callback(s_launcher_task_callback_data);
|
|
|
|
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
|
|
const uint8_t ams_cmd = prv_ams_command_for_music_command(MusicCommandPlay);
|
|
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
|
|
GAPLEClientKernel, true /* is_response_required */);
|
|
}
|
|
|
|
void test_ams__send_remote_command_non_kernel_main_task_then_disconnect(void) {
|
|
prv_connect_ams();
|
|
|
|
// Simulate Music app calling music_command_send():
|
|
stub_pebble_tasks_set_current(PebbleTask_App);
|
|
music_command_send(MusicCommandPlay);
|
|
|
|
// Simulate disconnecting:
|
|
ams_destroy();
|
|
|
|
// Process the KernelMain callback:
|
|
cl_assert(s_launcher_task_callback != NULL);
|
|
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
|
|
s_launcher_task_callback(s_launcher_task_callback_data);
|
|
|
|
// No crashes, no writes
|
|
fake_gatt_client_op_assert_no_write();
|
|
}
|
|
|
|
// Tests: music_needs_user_to_start_playback_on_phone
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__music_needs_user_to_start_playback_on_phone(void) {
|
|
prv_connect_ams();
|
|
|
|
// No metadata received yet, user needs to start playback on phone
|
|
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
|
|
|
|
// Received metadata
|
|
// cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false);
|
|
|
|
// Metadata cleared
|
|
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
|
|
}
|
|
|
|
// Tests: music_request_..._latency
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__music_request_reduced_latency(void) {
|
|
prv_connect_ams();
|
|
|
|
music_request_reduced_latency(true /* reduced_latency */);
|
|
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
|
|
ResponseTimeMiddle);
|
|
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].max_period_secs,
|
|
MAX_PERIOD_RUN_FOREVER);
|
|
|
|
music_request_reduced_latency(false /* reduced_latency */);
|
|
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
|
|
ResponseTimeMax);
|
|
}
|
|
|
|
void test_ams__music_request_low_latency_for_period(void) {
|
|
prv_connect_ams();
|
|
|
|
const uint32_t period_s = 1234;
|
|
music_request_low_latency_for_period(period_s * 1000);
|
|
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].state,
|
|
ResponseTimeMin);
|
|
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].max_period_secs,
|
|
period_s);
|
|
}
|
|
|
|
// Tests: Receiving Player updates (the happy paths)
|
|
///////////////////////////////////////////////////////////
|
|
|
|
static void prv_receive_entity_update(const uint8_t *update, uint16_t update_length) {
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_read_or_notification(entity_update, update, update_length, BLEGATTErrorSuccess);
|
|
}
|
|
|
|
void test_ams__receive_player_name_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// 0000 00 00 00 4d 75 73 69 63 ...Music
|
|
uint8_t player_name_update[] = {
|
|
0x00, 0x00, 0x00, 0x4d, 0x75, 0x73, 0x69, 0x63,
|
|
};
|
|
prv_receive_entity_update(player_name_update, sizeof(player_name_update));
|
|
|
|
char player_name[MUSIC_BUFFER_LENGTH];
|
|
cl_assert_equal_b(music_get_player_name(player_name), true);
|
|
cl_assert_equal_s(player_name, "Music");
|
|
}
|
|
|
|
void test_ams__receive_player_playback_info_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// Receive: playing, 200% playback rate, elapsed time 184.755s
|
|
// 0000 00 01 00 31 2c 32 2e 30 2c 31 38 34 2e 37 35 35 ...1,2.0 ,184.755
|
|
uint8_t playback_info_update[] = {
|
|
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
|
|
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
|
|
};
|
|
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
|
|
|
|
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePlaying);
|
|
|
|
// music_get_pos relies on having a sensible track duration, so simulate receiving this too:
|
|
// 02 03 00 33 31 39 2e 35 30 37 ...319.5 07
|
|
uint8_t track_duration_update[] = {
|
|
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
|
|
0x30, 0x37,
|
|
};
|
|
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
|
|
|
|
uint32_t track_pos_ms;
|
|
uint32_t track_duration_ms;
|
|
music_get_pos(&track_pos_ms, &track_duration_ms);
|
|
cl_assert_equal_i(track_pos_ms, 184755);
|
|
cl_assert_equal_i(track_duration_ms, 319507);
|
|
|
|
cl_assert_equal_i(music_get_playback_rate_percent(), 200);
|
|
}
|
|
|
|
void test_ams__receive_player_playback_info_update_nulls(void) {
|
|
prv_connect_ams();
|
|
|
|
// Receive: paused, empty string, empty string
|
|
// 0000 00 01 00 30 2c 2c ...0,,
|
|
uint8_t playback_info_update[] = {
|
|
0x00, 0x01, 0x00, 0x30, 0x2c, 0x2c,
|
|
};
|
|
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
|
|
|
|
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePaused);
|
|
|
|
uint32_t track_pos_ms;
|
|
uint32_t track_duration_ms;
|
|
music_get_pos(&track_pos_ms, &track_duration_ms);
|
|
cl_assert_equal_i(track_pos_ms, 0);
|
|
cl_assert_equal_i(track_duration_ms, 0);
|
|
|
|
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
|
|
}
|
|
|
|
void test_ams__receive_player_volume_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// Receive volume of 0.604925
|
|
// 00 02 00 30 2e 36 30 34 39 32 35 ...0.604 925
|
|
uint8_t volume_update[] = {
|
|
0x00, 0x02, 0x00, 0x30, 0x2e, 0x36, 0x30, 0x34,
|
|
0x39, 0x32, 0x35,
|
|
};
|
|
prv_receive_entity_update(volume_update, sizeof(volume_update));
|
|
|
|
cl_assert_equal_i(music_get_volume_percent(), 60);
|
|
}
|
|
|
|
// Tests: Receiving Player updates (the unhappy paths)
|
|
///////////////////////////////////////////////////////////
|
|
|
|
static void prv_assert_initial_playback_state(void) {
|
|
cl_assert_equal_i(music_get_playback_state(), MusicPlayStateUnknown);
|
|
|
|
uint32_t track_pos_ms;
|
|
uint32_t track_duration_ms;
|
|
music_get_pos(&track_pos_ms, &track_duration_ms);
|
|
cl_assert_equal_i(track_pos_ms, 0);
|
|
cl_assert_equal_i(track_duration_ms, 0);
|
|
|
|
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
|
|
}
|
|
|
|
void test_ams__receive_non_numeric_player_playback_info_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// Receive: 'A', 'B.0' playback rate, elapsed time 184.755s
|
|
// 0000 00 01 00 41 2c 42 2e 30 2c 31 38 34 2e 37 35 35 ...A,B.0 ,184.755
|
|
uint8_t nan_playback_info_update[] = {
|
|
0x00, 0x01, 0x00, 0x41, 0x2c, 0x42, 0x2e, 0x30,
|
|
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
|
|
};
|
|
prv_receive_entity_update(nan_playback_info_update, sizeof(nan_playback_info_update));
|
|
|
|
prv_assert_initial_playback_state();
|
|
}
|
|
|
|
void test_ams__receive_incomplete_csv_list_player_playback_info_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// Receive: playing, 200% playback rate
|
|
// 0000 00 01 00 31 2c 32 2e 30 ...1,2.0
|
|
uint8_t incomplete_playback_info_update[] = {
|
|
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
|
|
};
|
|
prv_receive_entity_update(incomplete_playback_info_update,
|
|
sizeof(incomplete_playback_info_update));
|
|
|
|
prv_assert_initial_playback_state();
|
|
}
|
|
|
|
void test_ams__receive_malformed_player_volume_update(void) {
|
|
prv_connect_ams();
|
|
|
|
cl_assert_equal_i(music_get_volume_percent(), 0);
|
|
|
|
// Receive volume of 0.604925
|
|
// 00 02 00 30 2e 41 30 34 39 32 35 ...0.A04 925
|
|
uint8_t volume_update[] = {
|
|
0x00, 0x02, 0x00, 0x30, 0x2e, 0x41, 0x30, 0x34,
|
|
0x39, 0x32, 0x35,
|
|
};
|
|
prv_receive_entity_update(volume_update, sizeof(volume_update));
|
|
|
|
cl_assert_equal_i(music_get_volume_percent(), 0);
|
|
}
|
|
|
|
// Tests: Receiving Track updates
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__receive_track_artist_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// 0000 02 00 00 4d 69 6c 65 73 20 44 61 76 69 73 ...Miles Davis
|
|
uint8_t track_artist_update[] = {
|
|
0x02, 0x00, 0x00, 0x4d, 0x69, 0x6c, 0x65, 0x73,
|
|
0x20, 0x44, 0x61, 0x76, 0x69, 0x73,
|
|
};
|
|
prv_receive_entity_update(track_artist_update, sizeof(track_artist_update));
|
|
|
|
char artist[MUSIC_BUFFER_LENGTH];
|
|
music_get_now_playing(NULL, artist, NULL);
|
|
|
|
cl_assert_equal_s(artist, "Miles Davis");
|
|
}
|
|
|
|
void test_ams__receive_track_title_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// 0000 02 02 00 53 6f 20 57 68 61 74 ...So Wh at
|
|
uint8_t track_title_update[] = {
|
|
0x02, 0x02, 0x00, 0x53, 0x6f, 0x20, 0x57, 0x68,
|
|
0x61, 0x74,
|
|
};
|
|
prv_receive_entity_update(track_title_update, sizeof(track_title_update));
|
|
|
|
char title[MUSIC_BUFFER_LENGTH];
|
|
music_get_now_playing(title, NULL, NULL);
|
|
|
|
cl_assert_equal_s(title, "So What");
|
|
}
|
|
|
|
void test_ams__receive_track_album_update(void) {
|
|
prv_connect_ams();
|
|
|
|
// 0000 02 01 00 4b 69 6e 64 20 4f 66 20 42 6c 75 65 20 ...Kind Of Blue
|
|
// 0010 28 4c 65 67 61 63 79 20 45 64 69 74 69 6f 6e 29 (Legacy Edition)
|
|
uint8_t track_album_update[] = {
|
|
0x02, 0x01, 0x00, 0x4b, 0x69, 0x6e, 0x64, 0x20,
|
|
0x4f, 0x66, 0x20, 0x42, 0x6c, 0x75, 0x65, 0x20,
|
|
0x28, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20,
|
|
0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29,
|
|
};
|
|
prv_receive_entity_update(track_album_update, sizeof(track_album_update));
|
|
|
|
char album[MUSIC_BUFFER_LENGTH];
|
|
music_get_now_playing(NULL, NULL, album);
|
|
|
|
cl_assert_equal_s(album, "Kind Of Blue (Legacy Edition)");
|
|
}
|
|
|
|
// Tests: Music service capabilities
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__supported_capabilities(void) {
|
|
prv_connect_ams();
|
|
|
|
// music_is_progress_reporting_supported() relies on a valid track duration
|
|
uint8_t track_duration_update[] = {
|
|
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
|
|
0x30, 0x37,
|
|
};
|
|
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
|
|
|
|
cl_assert_equal_b(music_is_playback_state_reporting_supported(), true);
|
|
cl_assert_equal_b(music_is_progress_reporting_supported(), true);
|
|
cl_assert_equal_b(music_is_volume_reporting_supported(), true);
|
|
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
|
|
for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
|
|
cl_assert_equal_b(music_is_command_supported(cmd), true);
|
|
}
|
|
|
|
cl_assert_equal_b(music_is_command_supported(NumMusicCommand), false);
|
|
}
|
|
|
|
// Tests: Create & Destroy
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__create_again_trips_assert(void) {
|
|
cl_assert_passert(ams_create());
|
|
}
|
|
|
|
void test_ams__create_works_again_after_destroy(void) {
|
|
ams_destroy();
|
|
ams_create();
|
|
// No assert hit.
|
|
}
|
|
|
|
void test_ams__destroy_after_destroy_is_fine(void) {
|
|
ams_destroy();
|
|
ams_destroy();
|
|
// No assert hit.
|
|
}
|
|
|
|
void test_ams__destroy_disconnects_from_music_service(void) {
|
|
prv_discover_ams(1 /* num_instances */);
|
|
|
|
// Simulate successful subscription:
|
|
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
|
|
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
|
|
|
|
ams_destroy();
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
}
|
|
|
|
// Test setup
|
|
///////////////////////////////////////////////////////////
|
|
|
|
void test_ams__initialize(void) {
|
|
s_launcher_task_callback = NULL;
|
|
memset(s_conn_mgr_states, 0, sizeof(s_conn_mgr_states));
|
|
fake_rtc_init(1234, 5678);
|
|
fake_event_init();
|
|
fake_gatt_client_subscriptions_init();
|
|
fake_gatt_client_op_init();
|
|
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
|
|
ams_create();
|
|
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
|
|
}
|
|
|
|
void test_ams__cleanup(void) {
|
|
ams_destroy();
|
|
fake_gatt_client_op_deinit();
|
|
fake_gatt_client_subscriptions_deinit();
|
|
}
|