mirror of
https://github.com/google/pebble.git
synced 2025-03-21 19:31:20 +00:00
431 lines
15 KiB
C
431 lines
15 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 "services/normal/music.h"
|
|
#include "services/normal/music_endpoint.h"
|
|
#include "services/normal/music_internal.h"
|
|
|
|
#include "services/common/comm_session/session.h"
|
|
#include "services/common/comm_session/session_remote_os.h"
|
|
|
|
#include "kernel/events.h"
|
|
|
|
#include "util/size.h"
|
|
|
|
// Stubs & Fakes
|
|
///////////////////////////////////////////////////////////
|
|
|
|
#include "fake_events.h"
|
|
#include "fake_rtc.h"
|
|
#include "fake_session.h"
|
|
#include "fake_system_task.h"
|
|
|
|
#include "stubs_app_manager.h"
|
|
#include "stubs_app_install_manager.h"
|
|
#include "stubs_passert.h"
|
|
#include "stubs_pbl_malloc.h"
|
|
#include "stubs_bt_lock.h"
|
|
#include "stubs_hexdump.h"
|
|
#include "stubs_logging.h"
|
|
#include "stubs_mutex.h"
|
|
#include "stubs_serial.h"
|
|
#include "stubs_tick.h"
|
|
|
|
extern void music_protocol_msg_callback(CommSession *session, const uint8_t* msg, size_t length);
|
|
|
|
// Helpers
|
|
///////////////////////////////////////////////////////////
|
|
|
|
static void prv_receive_app_info_event(bool is_android) {
|
|
const PebbleRemoteAppInfoEvent app_info_event = (const PebbleRemoteAppInfoEvent) {
|
|
.os = is_android ? RemoteOSAndroid : RemoteOSiOS,
|
|
};
|
|
music_endpoint_handle_mobile_app_info_event(&app_info_event);
|
|
}
|
|
|
|
static void prv_receive_app_event(bool is_open) {
|
|
const PebbleCommSessionEvent app_event = (const PebbleCommSessionEvent) {
|
|
.is_open = is_open,
|
|
.is_system = true
|
|
};
|
|
music_endpoint_handle_mobile_app_event(&app_event);
|
|
}
|
|
|
|
static void prv_receive_pp_data(const uint8_t *data, uint16_t length) {
|
|
fake_event_clear_last();
|
|
music_protocol_msg_callback(NULL, data, length);
|
|
}
|
|
|
|
static void prv_receive_and_assert_now_playing(bool expect_is_handled) {
|
|
uint8_t msg[] = { 0x10, 3, 'o', 'n', 'e', 3, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e', 0xAA,
|
|
0x00, 0x00, 0x00, 0xAA, 0x00, 0xAA, 0x00 };
|
|
prv_receive_pp_data(msg, sizeof(msg));
|
|
|
|
PebbleEvent e = fake_event_get_last();
|
|
if (expect_is_handled) {
|
|
cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
|
|
cl_assert_equal_i(e.media.type, PebbleMediaEventTypeTrackPosChanged);
|
|
|
|
char artist[MUSIC_BUFFER_LENGTH];
|
|
char album[MUSIC_BUFFER_LENGTH];
|
|
char title[MUSIC_BUFFER_LENGTH];
|
|
|
|
music_get_now_playing(title, artist, album);
|
|
|
|
cl_assert_equal_s(artist, "one");
|
|
cl_assert_equal_s(album, "two");
|
|
cl_assert_equal_s(title, "three");
|
|
|
|
uint32_t track_position, track_duration;
|
|
music_get_pos(&track_position, &track_duration);
|
|
cl_assert_equal_i(track_duration, 0xAA);
|
|
} else {
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
}
|
|
|
|
static void prv_receive_and_assert_play_state(bool expect_is_handled) {
|
|
uint8_t msg[] = { 0x11, 0x01, 0xAA, 0x00, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x01, 0x01 };
|
|
prv_receive_pp_data(msg, sizeof(msg));
|
|
|
|
PebbleEvent e = fake_event_get_last();
|
|
if (expect_is_handled) {
|
|
cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
|
|
cl_assert_equal_i(e.media.type, PebbleMediaEventTypeTrackPosChanged);
|
|
|
|
cl_assert_equal_i(music_get_playback_state(), 0x01);
|
|
uint32_t track_position, track_duration;
|
|
music_get_pos(&track_position, &track_duration);
|
|
cl_assert_equal_i(track_position, 0xAA);
|
|
cl_assert_equal_i(music_get_playback_rate_percent(), 0xAA);
|
|
|
|
} else {
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
}
|
|
|
|
static void prv_receive_and_assert_volume_info(bool expect_is_handled) {
|
|
uint8_t msg[] = { 0x12, 0x33 };
|
|
prv_receive_pp_data(msg, sizeof(msg));
|
|
|
|
PebbleEvent e = fake_event_get_last();
|
|
if (expect_is_handled) {
|
|
cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
|
|
cl_assert_equal_i(e.media.type, PebbleMediaEventTypeVolumeChanged);
|
|
|
|
cl_assert_equal_i(music_get_volume_percent(), 0x33);
|
|
|
|
} else {
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
}
|
|
|
|
static void prv_receive_and_assert_player_info(bool expect_is_handled) {
|
|
uint8_t msg[] = { 0x13, 17, 'c', 'o', 'm', '.', 's', 'p', 'o', 't', 'i', 'f', 'y', '.', 'm',
|
|
'u', 's', 'i', 'c', 7, 'S', 'p', 'o', 't', 'i', 'f', 'y' };
|
|
prv_receive_pp_data(msg, sizeof(msg));
|
|
|
|
PebbleEvent e = fake_event_get_last();
|
|
if (expect_is_handled) {
|
|
cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
|
|
cl_assert_equal_i(e.media.type, PebbleMediaEventTypeNowPlayingChanged);
|
|
|
|
char player_name[MUSIC_BUFFER_LENGTH];
|
|
music_get_player_name(player_name);
|
|
cl_assert_equal_s(player_name, "Spotify");
|
|
|
|
} else {
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
}
|
|
|
|
static void prv_receive_and_assert_all(bool expect_is_handled) {
|
|
prv_receive_and_assert_now_playing(expect_is_handled);
|
|
prv_receive_and_assert_play_state(expect_is_handled);
|
|
prv_receive_and_assert_volume_info(expect_is_handled);
|
|
prv_receive_and_assert_player_info(expect_is_handled);
|
|
}
|
|
|
|
|
|
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 */);
|
|
}
|
|
|
|
static void prv_assert_no_data_sent_cb(uint16_t endpoint_id,
|
|
const uint8_t* data, unsigned int data_length) {
|
|
cl_assert(false);
|
|
}
|
|
|
|
static bool s_now_playing_requested;
|
|
static void prv_assert_now_playing_requested_cb(uint16_t endpoint_id,
|
|
const uint8_t* data, unsigned int data_length) {
|
|
cl_assert_equal_i(data_length, 1);
|
|
cl_assert_equal_i(data[0], 0x08); // MusicEndpointCmdIDGetAllInfo
|
|
s_now_playing_requested = true;
|
|
}
|
|
|
|
static bool s_next_track_command_sent;
|
|
static void prv_assert_next_track_command_sent_cb(uint16_t endpoint_id,
|
|
const uint8_t* data, unsigned int data_length) {
|
|
cl_assert_equal_i(data_length, 1);
|
|
cl_assert_equal_i(data[0], 0x04); // MusicEndpointCmdIDNextTrack
|
|
s_next_track_command_sent = true;
|
|
}
|
|
|
|
static bool s_is_playback_cmd_sent;
|
|
static uint8_t s_playback_cmd_sent;
|
|
static void prv_assert_playback_command_sent_cb(uint16_t endpoint_id,
|
|
const uint8_t* data, unsigned int data_length) {
|
|
cl_assert_equal_i(data_length, 1);
|
|
if (!s_is_playback_cmd_sent) {
|
|
s_playback_cmd_sent = data[0];
|
|
s_is_playback_cmd_sent = true;
|
|
} else {
|
|
// Playback command is always followed by:
|
|
cl_assert_equal_i(data[0], 0x08); // MusicEndpointCmdIDGetAllInfo
|
|
}
|
|
}
|
|
|
|
// Tests
|
|
///////////////////////////////////////////////////////////
|
|
|
|
Transport *s_transport;
|
|
|
|
void test_music_endpoint__initialize(void) {
|
|
s_now_playing_requested = false;
|
|
s_next_track_command_sent = false;
|
|
s_is_playback_cmd_sent = false;
|
|
s_playback_cmd_sent = ~0;
|
|
fake_event_init();
|
|
fake_rtc_init(0, 0);
|
|
fake_comm_session_init();
|
|
music_init();
|
|
|
|
s_transport = fake_transport_create(TransportDestinationSystem, NULL, NULL);
|
|
fake_transport_set_connected(s_transport, true /* connected */);
|
|
|
|
// Simulate connecting Pebble mobile app:
|
|
prv_receive_app_event(true /* is_open */);
|
|
}
|
|
|
|
void test_music_endpoint__cleanup(void) {
|
|
fake_comm_session_process_send_next();
|
|
|
|
// Simulate disconnecting Pebble mobile app:
|
|
prv_receive_app_event(false /* is_open */);
|
|
|
|
fake_comm_session_cleanup();
|
|
}
|
|
|
|
void test_music_endpoint__ignore_now_playing_while_not_connected(void) {
|
|
// Don't connect app, but receive Now Playing info. Should be ignored:
|
|
prv_receive_and_assert_all(false /* expect_is_handled */);
|
|
}
|
|
|
|
void test_music_endpoint__ignore_now_playing_while_other_server_connected(void) {
|
|
// Another music server connects:
|
|
prv_set_dummy_server_connected(true /* connected */);
|
|
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
|
|
// Receive Now Playing info. Should be ignored, because other server is connected:
|
|
prv_receive_and_assert_all(false /* expect_is_handled*/);
|
|
|
|
// Disconnect dummy server, to clean up after ourselves:
|
|
prv_set_dummy_server_connected(false /* connected */);
|
|
}
|
|
|
|
void test_music_endpoint__ignore_now_playing_from_ios_app(void) {
|
|
// iOS app connects:
|
|
prv_receive_app_info_event(false /* is_android */);
|
|
// iOS app is not supposed to use this endpoint:
|
|
prv_receive_and_assert_all(false /* expect_is_handled*/);
|
|
}
|
|
|
|
void test_music_endpoint__request_now_playing_upon_connect(void) {
|
|
fake_transport_set_sent_cb(s_transport, &prv_assert_now_playing_requested_cb);
|
|
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
|
|
fake_comm_session_process_send_next();
|
|
cl_assert_equal_b(s_now_playing_requested, true);
|
|
}
|
|
|
|
void test_music_endpoint__receive_now_playing_while_connected(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
prv_receive_and_assert_all(true /* expect_is_handled*/);
|
|
}
|
|
|
|
void test_music_endpoint__ignore_unknown_message(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
uint8_t unknown_msg[] = { 0xff };
|
|
prv_receive_pp_data(unknown_msg, sizeof(unknown_msg));
|
|
PebbleEvent e = fake_event_get_last();
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
|
|
void test_music_endpoint__receive_zero_length_now_playing(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
prv_receive_and_assert_all(true /* expect_is_handled*/);
|
|
cl_assert_equal_b(music_has_now_playing(), true);
|
|
|
|
uint8_t zero_length_now_playing[] = { 0x10, 0, 0, 0 };
|
|
prv_receive_pp_data(zero_length_now_playing, sizeof(zero_length_now_playing));
|
|
cl_assert_equal_b(music_has_now_playing(), false);
|
|
}
|
|
|
|
void test_music_endpoint__ignore_malformatted_messages(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
const uint8_t malformatted_artist[] = {
|
|
0x10, 14, 'o', 'n', 'e', 3, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e'
|
|
};
|
|
const uint8_t malformatted_album[] = {
|
|
0x10, 3, 'o', 'n', 'e', 10, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e'
|
|
};
|
|
const uint8_t malformatted_title[] = {
|
|
0x10, 3, 'o', 'n', 'e', 3, 't', 'w', 'o', 6, 't', 'h', 'r', 'e', 'e'
|
|
};
|
|
const uint8_t malformatted_player[] = {
|
|
0x13, 17, 'c', 'o', 'm', '.', 's', 'p', 'o', 't', 'i', 'f', 'y', '.', 'm', 'u', 's', 'i', 'c',
|
|
9, 'S', 'p', 'o', 't', 'i', 'f', 'y'
|
|
};
|
|
struct {
|
|
const uint8_t *data;
|
|
uint16_t length;
|
|
} test_vectors[] = {
|
|
{ malformatted_artist, sizeof(malformatted_artist) },
|
|
{ malformatted_album, sizeof(malformatted_album) },
|
|
{ malformatted_title, sizeof(malformatted_title) },
|
|
{ malformatted_player, sizeof(malformatted_player) }
|
|
};
|
|
for (int i = 0; i < ARRAY_LENGTH(test_vectors); ++i) {
|
|
prv_receive_pp_data(test_vectors[i].data, test_vectors[i].length);
|
|
PebbleEvent e = fake_event_get_last();
|
|
cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
|
|
}
|
|
}
|
|
|
|
void test_music_endpoint__supported_capabilities(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
// music_is_progress_reporting_supported() relies on a valid track duration
|
|
prv_receive_and_assert_all(true /* expect_is_handled*/);
|
|
|
|
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(), false);
|
|
for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
|
|
bool expect_supported = true;
|
|
if (cmd == MusicCommandAdvanceRepeatMode ||
|
|
cmd == MusicCommandAdvanceShuffleMode ||
|
|
cmd == MusicCommandSkipForward ||
|
|
cmd == MusicCommandSkipBackward ||
|
|
cmd == MusicCommandLike ||
|
|
cmd == MusicCommandDislike ||
|
|
cmd == MusicCommandBookmark) {
|
|
expect_supported = false;
|
|
}
|
|
cl_assert_equal_b(music_is_command_supported(cmd), expect_supported);
|
|
}
|
|
}
|
|
|
|
void test_music_endpoint__reduced_latency(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
|
|
cl_assert_equal_b(fake_comm_session_is_latency_reduced(), false);
|
|
music_request_reduced_latency(true);
|
|
cl_assert_equal_b(fake_comm_session_is_latency_reduced(), true);
|
|
music_request_reduced_latency(false);
|
|
cl_assert_equal_b(fake_comm_session_is_latency_reduced(), false);
|
|
}
|
|
|
|
void test_music_endpoint__low_latency_for_period(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
|
|
cl_assert_equal_i(fake_comm_session_get_responsiveness_max_period(), 0);
|
|
music_request_low_latency_for_period(2000);
|
|
cl_assert_equal_i(fake_comm_session_get_responsiveness_max_period(), 2);
|
|
}
|
|
|
|
void test_music_endpoint__send_unsupported_command(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
fake_comm_session_process_send_next(); // send out any pending data
|
|
|
|
// Attempting to send an unsupported command should not result in any data getting sent out:
|
|
fake_transport_set_sent_cb(s_transport, prv_assert_no_data_sent_cb);
|
|
music_command_send(MusicCommandAdvanceRepeatMode);
|
|
fake_comm_session_process_send_next();
|
|
}
|
|
|
|
void test_music_endpoint__send_next_track_command(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
fake_comm_session_process_send_next(); // send out any pending data
|
|
|
|
fake_transport_set_sent_cb(s_transport, prv_assert_next_track_command_sent_cb);
|
|
music_command_send(MusicCommandNextTrack);
|
|
fake_comm_session_process_send_next();
|
|
|
|
cl_assert_equal_b(s_next_track_command_sent, true);
|
|
}
|
|
|
|
void test_music_endpoint__send_playback_command(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
fake_comm_session_process_send_next(); // send out any pending data
|
|
|
|
MusicCommand cmds[] = {
|
|
MusicCommandTogglePlayPause,
|
|
MusicCommandPause,
|
|
MusicCommandPlay,
|
|
};
|
|
|
|
uint8_t opcodes[] = {
|
|
0x1,
|
|
0x2,
|
|
0x3,
|
|
};
|
|
|
|
fake_transport_set_sent_cb(s_transport, prv_assert_playback_command_sent_cb);
|
|
for (int i = 0; i < ARRAY_LENGTH(cmds); ++i) {
|
|
music_command_send(cmds[i]);
|
|
fake_comm_session_process_send_next();
|
|
cl_assert_equal_i(s_is_playback_cmd_sent, true);
|
|
cl_assert_equal_i(s_playback_cmd_sent, opcodes[i]);
|
|
s_is_playback_cmd_sent = false;
|
|
}
|
|
}
|
|
|
|
void test_music_endpoint__player_name_not_available(void) {
|
|
// Android app connects:
|
|
prv_receive_app_info_event(true /* is_android */);
|
|
|
|
cl_assert_equal_b(music_get_player_name(NULL), false);
|
|
}
|