/*
* Copyright 2018 The Android Open Source Project
*
* 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 "device.h"
#include
#include "connection_handler.h"
#include "packet/avrcp/avrcp_reject_packet.h"
#include "packet/avrcp/general_reject_packet.h"
#include "packet/avrcp/get_play_status_packet.h"
#include "packet/avrcp/pass_through_packet.h"
#include "packet/avrcp/set_absolute_volume.h"
#include "packet/avrcp/set_addressed_player.h"
#include "stack_config.h"
namespace bluetooth {
namespace avrcp {
#define DEVICE_LOG(LEVEL) LOG(LEVEL) << address_.ToString() << " : "
#define DEVICE_VLOG(LEVEL) VLOG(LEVEL) << address_.ToString() << " : "
#define VOL_NOT_SUPPORTED -1
#define VOL_REGISTRATION_FAILED -2
Device::Device(
const RawAddress& bdaddr, bool avrcp13_compatibility,
base::Callback message)>
send_msg_cb,
uint16_t ctrl_mtu, uint16_t browse_mtu)
: weak_ptr_factory_(this),
address_(bdaddr),
avrcp13_compatibility_(avrcp13_compatibility),
send_message_cb_(send_msg_cb),
ctrl_mtu_(ctrl_mtu),
browse_mtu_(browse_mtu) {}
void Device::RegisterInterfaces(MediaInterface* media_interface,
A2dpInterface* a2dp_interface,
VolumeInterface* volume_interface) {
CHECK(media_interface);
CHECK(a2dp_interface);
a2dp_interface_ = a2dp_interface;
media_interface_ = media_interface;
volume_interface_ = volume_interface;
}
base::WeakPtr Device::Get() {
return weak_ptr_factory_.GetWeakPtr();
}
void Device::SetBrowseMtu(uint16_t browse_mtu) {
DEVICE_LOG(INFO) << __PRETTY_FUNCTION__ << ": browse_mtu = " << browse_mtu;
browse_mtu_ = browse_mtu;
}
bool Device::IsActive() const {
return address_ == a2dp_interface_->active_peer();
}
bool Device::IsInSilenceMode() const {
return a2dp_interface_->is_peer_in_silence_mode(address_);
}
void Device::VendorPacketHandler(uint8_t label,
std::shared_ptr pkt) {
CHECK(media_interface_);
DEVICE_VLOG(3) << __func__ << ": pdu=" << pkt->GetCommandPdu();
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(static_cast(0), Status::INVALID_COMMAND);
send_message(label, false, std::move(response));
return;
}
// All CTypes at and above NOT_IMPLEMENTED are all response types.
if (pkt->GetCType() == CType::NOT_IMPLEMENTED) {
return;
}
if (pkt->GetCType() >= CType::ACCEPTED) {
switch (pkt->GetCommandPdu()) {
// VOLUME_CHANGED is the only notification we register for while target.
case CommandPdu::REGISTER_NOTIFICATION: {
auto register_notification =
Packet::Specialize(pkt);
if (!register_notification->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response =
RejectBuilder::MakeBuilder(pkt->GetCommandPdu(),
Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
active_labels_.erase(label);
volume_interface_ = nullptr;
volume_ = VOL_REGISTRATION_FAILED;
return;
}
if (register_notification->GetEvent() != Event::VOLUME_CHANGED) {
DEVICE_LOG(WARNING)
<< __func__ << ": Unhandled register notification received: "
<< register_notification->GetEvent();
return;
}
HandleVolumeChanged(label, register_notification);
break;
}
case CommandPdu::SET_ABSOLUTE_VOLUME:
// TODO (apanicke): Add a retry mechanism if the response has a
// different volume than the one we set. For now, we don't care
// about the response to this message.
break;
default:
DEVICE_LOG(WARNING)
<< __func__ << ": Unhandled Response: pdu=" << pkt->GetCommandPdu();
break;
}
return;
}
switch (pkt->GetCommandPdu()) {
case CommandPdu::GET_CAPABILITIES: {
HandleGetCapabilities(label,
Packet::Specialize(pkt));
} break;
case CommandPdu::REGISTER_NOTIFICATION: {
HandleNotification(label,
Packet::Specialize(pkt));
} break;
case CommandPdu::GET_ELEMENT_ATTRIBUTES: {
auto get_element_attributes_request_pkt = Packet::Specialize(pkt);
if (!get_element_attributes_request_pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(pkt->GetCommandPdu(), Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
}
media_interface_->GetSongInfo(base::Bind(&Device::GetElementAttributesResponse, weak_ptr_factory_.GetWeakPtr(),
label, get_element_attributes_request_pkt));
} break;
case CommandPdu::GET_PLAY_STATUS: {
media_interface_->GetPlayStatus(base::Bind(&Device::GetPlayStatusResponse,
weak_ptr_factory_.GetWeakPtr(),
label));
} break;
case CommandPdu::PLAY_ITEM: {
HandlePlayItem(label, Packet::Specialize(pkt));
} break;
case CommandPdu::SET_ADDRESSED_PLAYER: {
// TODO (apanicke): Implement set addressed player. We don't need
// this currently since the current implementation only has one
// player and the player will never change, but we need it for a
// more complete implementation.
auto set_addressed_player_request = Packet::Specialize(pkt);
if (!set_addressed_player_request->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(pkt->GetCommandPdu(), Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
return;
}
media_interface_->GetMediaPlayerList(base::Bind(&Device::HandleSetAddressedPlayer, weak_ptr_factory_.GetWeakPtr(),
label, set_addressed_player_request));
} break;
default: {
DEVICE_LOG(ERROR) << "Unhandled Vendor Packet: " << pkt->ToString();
auto response = RejectBuilder::MakeBuilder(
(CommandPdu)pkt->GetCommandPdu(), Status::INVALID_COMMAND);
send_message(label, false, std::move(response));
} break;
}
}
void Device::HandleGetCapabilities(
uint8_t label, const std::shared_ptr& pkt) {
DEVICE_VLOG(4) << __func__
<< ": capability=" << pkt->GetCapabilityRequested();
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(pkt->GetCommandPdu(), Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
return;
}
switch (pkt->GetCapabilityRequested()) {
case Capability::COMPANY_ID: {
auto response =
GetCapabilitiesResponseBuilder::MakeCompanyIdBuilder(0x001958);
response->AddCompanyId(0x002345);
send_message_cb_.Run(label, false, std::move(response));
} break;
case Capability::EVENTS_SUPPORTED: {
auto response =
GetCapabilitiesResponseBuilder::MakeEventsSupportedBuilder(
Event::PLAYBACK_STATUS_CHANGED);
response->AddEvent(Event::TRACK_CHANGED);
response->AddEvent(Event::PLAYBACK_POS_CHANGED);
if (!avrcp13_compatibility_) {
response->AddEvent(Event::AVAILABLE_PLAYERS_CHANGED);
response->AddEvent(Event::ADDRESSED_PLAYER_CHANGED);
response->AddEvent(Event::UIDS_CHANGED);
response->AddEvent(Event::NOW_PLAYING_CONTENT_CHANGED);
}
send_message(label, false, std::move(response));
} break;
default: {
DEVICE_LOG(WARNING) << "Unhandled Capability: "
<< pkt->GetCapabilityRequested();
auto response = RejectBuilder::MakeBuilder(CommandPdu::GET_CAPABILITIES,
Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
} break;
}
}
void Device::HandleNotification(
uint8_t label, const std::shared_ptr& pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(pkt->GetCommandPdu(),
Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
return;
}
DEVICE_VLOG(4) << __func__ << ": event=" << pkt->GetEventRegistered();
switch (pkt->GetEventRegistered()) {
case Event::TRACK_CHANGED: {
media_interface_->GetNowPlayingList(
base::Bind(&Device::TrackChangedNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), label, true));
} break;
case Event::PLAYBACK_STATUS_CHANGED: {
media_interface_->GetPlayStatus(
base::Bind(&Device::PlaybackStatusNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), label, true));
} break;
case Event::PLAYBACK_POS_CHANGED: {
play_pos_interval_ = pkt->GetInterval();
media_interface_->GetPlayStatus(
base::Bind(&Device::PlaybackPosNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), label, true));
} break;
case Event::NOW_PLAYING_CONTENT_CHANGED: {
media_interface_->GetNowPlayingList(
base::Bind(&Device::HandleNowPlayingNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), label, true));
} break;
case Event::AVAILABLE_PLAYERS_CHANGED: {
// TODO (apanicke): If we make a separate handler function for this, make
// sure to register the notification in the interim response.
// Respond immediately since this notification doesn't require any info
avail_players_changed_ = Notification(true, label);
auto response =
RegisterNotificationResponseBuilder::MakeAvailablePlayersBuilder(
true);
send_message(label, false, std::move(response));
} break;
case Event::ADDRESSED_PLAYER_CHANGED: {
media_interface_->GetMediaPlayerList(
base::Bind(&Device::AddressedPlayerNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), label, true));
} break;
case Event::UIDS_CHANGED: {
// TODO (apanicke): If we make a separate handler function for this, make
// sure to register the notification in the interim response.
// Respond immediately since this notification doesn't require any info
uids_changed_ = Notification(true, label);
auto response =
RegisterNotificationResponseBuilder::MakeUidsChangedBuilder(true, 0);
send_message(label, false, std::move(response));
} break;
default: {
DEVICE_LOG(ERROR) << __func__ << " : Unknown event registered. Event ID="
<< pkt->GetEventRegistered();
auto response = RejectBuilder::MakeBuilder(
(CommandPdu)pkt->GetCommandPdu(), Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
} break;
}
}
void Device::RegisterVolumeChanged() {
DEVICE_VLOG(2) << __func__;
if (volume_interface_ == nullptr) return;
auto request =
RegisterNotificationRequestBuilder::MakeBuilder(Event::VOLUME_CHANGED, 0);
// Find an open transaction label to prevent conflicts with other commands
// that are in flight. We can not use the reserved label while the
// notification hasn't been completed.
uint8_t label = MAX_TRANSACTION_LABEL;
for (uint8_t i = 0; i < MAX_TRANSACTION_LABEL; i++) {
if (active_labels_.find(i) == active_labels_.end()) {
active_labels_.insert(i);
label = i;
break;
}
}
if (label == MAX_TRANSACTION_LABEL) {
DEVICE_LOG(FATAL)
<< __func__
<< ": Abandon all hope, something went catastrophically wrong";
}
send_message_cb_.Run(label, false, std::move(request));
}
void Device::HandleVolumeChanged(
uint8_t label, const std::shared_ptr& pkt) {
DEVICE_VLOG(1) << __func__ << ": interim=" << pkt->IsInterim();
if (volume_interface_ == nullptr) return;
if (pkt->GetCType() == CType::REJECTED) {
// Disable Absolute Volume
active_labels_.erase(label);
volume_interface_ = nullptr;
volume_ = VOL_REGISTRATION_FAILED;
return;
}
// We only update on interim and just re-register on changes.
if (!pkt->IsInterim()) {
active_labels_.erase(label);
RegisterVolumeChanged();
return;
}
// Handle the first volume update.
if (volume_ == VOL_NOT_SUPPORTED) {
volume_ = pkt->GetVolume();
volume_interface_->DeviceConnected(
GetAddress(),
base::Bind(&Device::SetVolume, weak_ptr_factory_.GetWeakPtr()));
// Ignore the returned volume in favor of the volume returned
// by the volume interface.
return;
}
if (!IsActive()) {
DEVICE_VLOG(3) << __func__
<< ": Ignoring volume changes from non active device";
return;
}
volume_ = pkt->GetVolume();
DEVICE_VLOG(1) << __func__ << ": Volume has changed to " << (uint32_t)volume_;
volume_interface_->SetVolume(volume_);
}
void Device::SetVolume(int8_t volume) {
// TODO (apanicke): Implement logic for Multi-AVRCP
DEVICE_VLOG(1) << __func__ << ": volume=" << (int)volume;
auto request = SetAbsoluteVolumeRequestBuilder::MakeBuilder(volume);
uint8_t label = MAX_TRANSACTION_LABEL;
for (uint8_t i = 0; i < MAX_TRANSACTION_LABEL; i++) {
if (active_labels_.find(i) == active_labels_.end()) {
active_labels_.insert(i);
label = i;
break;
}
}
volume_ = volume;
send_message_cb_.Run(label, false, std::move(request));
}
void Device::TrackChangedNotificationResponse(uint8_t label, bool interim,
std::string curr_song_id,
std::vector song_list) {
DEVICE_VLOG(1) << __func__;
uint64_t uid = 0;
if (interim) {
track_changed_ = Notification(true, label);
} else if (!track_changed_.first) {
DEVICE_VLOG(0) << __func__ << ": Device not registered for update";
return;
}
// Anytime we use the now playing list, update our map so that its always
// current
now_playing_ids_.clear();
for (const SongInfo& song : song_list) {
now_playing_ids_.insert(song.media_id);
if (curr_song_id == song.media_id) {
DEVICE_VLOG(3) << __func__ << ": Found media ID match for "
<< song.media_id;
uid = now_playing_ids_.get_uid(curr_song_id);
}
}
if (curr_song_id == "") {
DEVICE_LOG(WARNING) << "Empty media ID";
uid = 0;
if (stack_config_get_interface()->get_pts_avrcp_test()) {
DEVICE_LOG(WARNING) << __func__ << ": pts test mode";
uid = 0xffffffffffffffff;
}
}
auto response = RegisterNotificationResponseBuilder::MakeTrackChangedBuilder(
interim, uid);
send_message_cb_.Run(label, false, std::move(response));
if (!interim) {
active_labels_.erase(label);
track_changed_ = Notification(false, 0);
}
}
void Device::PlaybackStatusNotificationResponse(uint8_t label, bool interim,
PlayStatus status) {
DEVICE_VLOG(1) << __func__;
if (status.state == PlayState::PAUSED) play_pos_update_cb_.Cancel();
if (interim) {
play_status_changed_ = Notification(true, label);
} else if (!play_status_changed_.first) {
DEVICE_VLOG(0) << __func__ << ": Device not registered for update";
return;
}
auto state_to_send = status.state;
if (!IsActive()) state_to_send = PlayState::PAUSED;
if (!interim && state_to_send == last_play_status_.state) {
DEVICE_VLOG(0) << __func__
<< ": Not sending notification due to no state update "
<< address_.ToString();
return;
}
last_play_status_.state = state_to_send;
auto response =
RegisterNotificationResponseBuilder::MakePlaybackStatusBuilder(
interim, IsActive() ? status.state : PlayState::PAUSED);
send_message_cb_.Run(label, false, std::move(response));
if (!interim) {
active_labels_.erase(label);
play_status_changed_ = Notification(false, 0);
}
}
void Device::PlaybackPosNotificationResponse(uint8_t label, bool interim,
PlayStatus status) {
DEVICE_VLOG(4) << __func__;
if (interim) {
play_pos_changed_ = Notification(true, label);
} else if (!play_pos_changed_.first) {
DEVICE_VLOG(3) << __func__ << ": Device not registered for update";
return;
}
if (!interim && last_play_status_.position == status.position) {
DEVICE_LOG(WARNING) << address_.ToString()
<< ": No update to play position";
return;
}
auto response =
RegisterNotificationResponseBuilder::MakePlaybackPositionBuilder(
interim, status.position);
send_message_cb_.Run(label, false, std::move(response));
last_play_status_.position = status.position;
if (!interim) {
active_labels_.erase(label);
play_pos_changed_ = Notification(false, 0);
}
// We still try to send updates while music is playing to the non active
// device even though the device thinks the music is paused. This makes
// the status bar on the remote device move.
if (status.state == PlayState::PLAYING && !IsInSilenceMode()) {
DEVICE_VLOG(0) << __func__ << ": Queue next play position update";
play_pos_update_cb_.Reset(base::Bind(&Device::HandlePlayPosUpdate,
weak_ptr_factory_.GetWeakPtr()));
base::MessageLoop::current()->task_runner()->PostDelayedTask(
FROM_HERE, play_pos_update_cb_.callback(),
base::TimeDelta::FromSeconds(play_pos_interval_));
}
}
// TODO (apanicke): Finish implementing when we add support for more than one
// player
void Device::AddressedPlayerNotificationResponse(
uint8_t label, bool interim, uint16_t curr_player,
std::vector /* unused */) {
DEVICE_VLOG(1) << __func__
<< ": curr_player_id=" << (unsigned int)curr_player;
if (interim) {
addr_player_changed_ = Notification(true, label);
} else if (!addr_player_changed_.first) {
DEVICE_VLOG(3) << __func__ << ": Device not registered for update";
return;
}
// If there is no set browsed player, use the current addressed player as the
// default NOTE: Using any browsing commands before the browsed player is set
// is a violation of the AVRCP Spec but there are some carkits that try too
// anyways
if (curr_browsed_player_id_ == -1) curr_browsed_player_id_ = curr_player;
auto response =
RegisterNotificationResponseBuilder::MakeAddressedPlayerBuilder(
interim, curr_player, 0x0000);
send_message_cb_.Run(label, false, std::move(response));
if (!interim) {
active_labels_.erase(label);
addr_player_changed_ = Notification(false, 0);
RejectNotification();
}
}
void Device::RejectNotification() {
DEVICE_VLOG(1) << __func__;
Notification* rejectNotification[] = {&play_status_changed_, &track_changed_,
&play_pos_changed_,
&now_playing_changed_};
for (int i = 0; i < 4; i++) {
uint8_t label = rejectNotification[i]->second;
auto response = RejectBuilder::MakeBuilder(
CommandPdu::REGISTER_NOTIFICATION, Status::ADDRESSED_PLAYER_CHANGED);
send_message_cb_.Run(label, false, std::move(response));
active_labels_.erase(label);
rejectNotification[i] = new Notification(false, 0);
}
}
void Device::GetPlayStatusResponse(uint8_t label, PlayStatus status) {
DEVICE_VLOG(2) << __func__ << ": position=" << status.position
<< " duration=" << status.duration
<< " state=" << status.state;
auto response = GetPlayStatusResponseBuilder::MakeBuilder(
status.duration, status.position,
IsActive() ? status.state : PlayState::PAUSED);
send_message(label, false, std::move(response));
}
void Device::GetElementAttributesResponse(
uint8_t label, std::shared_ptr pkt,
SongInfo info) {
DEVICE_VLOG(2) << __func__;
auto get_element_attributes_pkt = pkt;
auto attributes_requested =
get_element_attributes_pkt->GetAttributesRequested();
auto response = GetElementAttributesResponseBuilder::MakeBuilder(ctrl_mtu_);
last_song_info_ = info;
if (attributes_requested.size() != 0) {
for (const auto& attribute : attributes_requested) {
if (info.attributes.find(attribute) != info.attributes.end()) {
response->AddAttributeEntry(*info.attributes.find(attribute));
}
}
} else { // zero attributes requested which means all attributes requested
for (const auto& attribute : info.attributes) {
response->AddAttributeEntry(attribute);
}
}
send_message(label, false, std::move(response));
}
void Device::MessageReceived(uint8_t label, std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(static_cast(0), Status::INVALID_COMMAND);
send_message(label, false, std::move(response));
return;
}
DEVICE_VLOG(4) << __func__ << ": opcode=" << pkt->GetOpcode();
active_labels_.insert(label);
switch (pkt->GetOpcode()) {
// TODO (apanicke): Remove handling of UNIT_INFO and SUBUNIT_INFO from
// the AVRC_API and instead handle it here to reduce fragmentation.
case Opcode::UNIT_INFO: {
} break;
case Opcode::SUBUNIT_INFO: {
} break;
case Opcode::PASS_THROUGH: {
auto pass_through_packet = Packet::Specialize(pkt);
if (!pass_through_packet->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(static_cast(0), Status::INVALID_COMMAND);
send_message(label, false, std::move(response));
return;
}
auto response = PassThroughPacketBuilder::MakeBuilder(
true, pass_through_packet->GetKeyState() == KeyState::PUSHED,
pass_through_packet->GetOperationId());
send_message(label, false, std::move(response));
// TODO (apanicke): Use an enum for media key ID's
if (pass_through_packet->GetOperationId() == 0x44 &&
pass_through_packet->GetKeyState() == KeyState::PUSHED) {
// We need to get the play status since we need to know
// what the actual playstate is without being modified
// by whether the device is active.
media_interface_->GetPlayStatus(base::Bind(
[](base::WeakPtr d, PlayStatus s) {
if (!d) return;
if (!d->IsActive()) {
LOG(INFO) << "Setting " << d->address_.ToString()
<< " to be the active device";
d->media_interface_->SetActiveDevice(d->address_);
if (s.state == PlayState::PLAYING) {
LOG(INFO)
<< "Skipping sendKeyEvent since music is already playing";
return;
}
}
d->media_interface_->SendKeyEvent(0x44, KeyState::PUSHED);
},
weak_ptr_factory_.GetWeakPtr()));
return;
}
if (IsActive()) {
media_interface_->SendKeyEvent(pass_through_packet->GetOperationId(),
pass_through_packet->GetKeyState());
}
} break;
case Opcode::VENDOR: {
auto vendor_pkt = Packet::Specialize(pkt);
VendorPacketHandler(label, vendor_pkt);
} break;
}
}
void Device::HandlePlayItem(uint8_t label,
std::shared_ptr pkt) {
DEVICE_VLOG(2) << __func__ << ": scope=" << pkt->GetScope()
<< " uid=" << pkt->GetUid();
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = RejectBuilder::MakeBuilder(pkt->GetCommandPdu(), Status::INVALID_PARAMETER);
send_message(label, false, std::move(response));
return;
}
std::string media_id = "";
switch (pkt->GetScope()) {
case Scope::NOW_PLAYING:
media_id = now_playing_ids_.get_media_id(pkt->GetUid());
break;
case Scope::VFS:
media_id = vfs_ids_.get_media_id(pkt->GetUid());
break;
default:
DEVICE_LOG(WARNING) << __func__ << ": Unknown scope for play item";
}
if (media_id == "") {
DEVICE_VLOG(2) << "Could not find item";
auto response = RejectBuilder::MakeBuilder(CommandPdu::PLAY_ITEM,
Status::DOES_NOT_EXIST);
send_message(label, false, std::move(response));
return;
}
media_interface_->PlayItem(curr_browsed_player_id_,
pkt->GetScope() == Scope::NOW_PLAYING, media_id);
auto response = PlayItemResponseBuilder::MakeBuilder(Status::NO_ERROR);
send_message(label, false, std::move(response));
}
void Device::HandleSetAddressedPlayer(
uint8_t label, std::shared_ptr pkt,
uint16_t curr_player, std::vector players) {
DEVICE_VLOG(2) << __func__ << ": PlayerId=" << pkt->GetPlayerId();
if (curr_player != pkt->GetPlayerId()) {
DEVICE_VLOG(2) << "Reject invalid addressed player ID";
auto response = RejectBuilder::MakeBuilder(CommandPdu::SET_ADDRESSED_PLAYER,
Status::INVALID_PLAYER_ID);
send_message(label, false, std::move(response));
return;
}
auto response =
SetAddressedPlayerResponseBuilder::MakeBuilder(Status::NO_ERROR);
send_message(label, false, std::move(response));
}
void Device::BrowseMessageReceived(uint8_t label,
std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = GeneralRejectBuilder::MakeBuilder(Status::INVALID_COMMAND);
send_message(label, false, std::move(response));
return;
}
DEVICE_VLOG(1) << __func__ << ": pdu=" << pkt->GetPdu();
switch (pkt->GetPdu()) {
case BrowsePdu::SET_BROWSED_PLAYER:
HandleSetBrowsedPlayer(label,
Packet::Specialize(pkt));
break;
case BrowsePdu::GET_FOLDER_ITEMS:
HandleGetFolderItems(label,
Packet::Specialize(pkt));
break;
case BrowsePdu::CHANGE_PATH:
HandleChangePath(label, Packet::Specialize(pkt));
break;
case BrowsePdu::GET_ITEM_ATTRIBUTES:
HandleGetItemAttributes(
label, Packet::Specialize(pkt));
break;
case BrowsePdu::GET_TOTAL_NUMBER_OF_ITEMS:
HandleGetTotalNumberOfItems(
label, Packet::Specialize(pkt));
break;
default:
DEVICE_LOG(WARNING) << __func__ << ": " << pkt->GetPdu();
auto response = GeneralRejectBuilder::MakeBuilder(Status::INVALID_COMMAND);
send_message(label, true, std::move(response));
break;
}
}
void Device::HandleGetFolderItems(uint8_t label,
std::shared_ptr pkt) {
if (!pkt->IsValid()) {
// The specific get folder items builder is unimportant on failure.
DEVICE_LOG(WARNING) << __func__ << ": Get folder items request packet is not valid";
auto response =
GetFolderItemsResponseBuilder::MakePlayerListBuilder(Status::INVALID_PARAMETER, 0x0000, browse_mtu_);
send_message(label, true, std::move(response));
return;
}
DEVICE_VLOG(2) << __func__ << ": scope=" << pkt->GetScope();
switch (pkt->GetScope()) {
case Scope::MEDIA_PLAYER_LIST:
media_interface_->GetMediaPlayerList(
base::Bind(&Device::GetMediaPlayerListResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
break;
case Scope::VFS:
media_interface_->GetFolderItems(
curr_browsed_player_id_, CurrentFolder(),
base::Bind(&Device::GetVFSListResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
break;
case Scope::NOW_PLAYING:
media_interface_->GetNowPlayingList(
base::Bind(&Device::GetNowPlayingListResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
break;
default:
DEVICE_LOG(ERROR) << __func__ << ": " << pkt->GetScope();
auto response = GetFolderItemsResponseBuilder::MakePlayerListBuilder(Status::INVALID_PARAMETER, 0, browse_mtu_);
send_message(label, true, std::move(response));
break;
}
}
void Device::HandleGetTotalNumberOfItems(
uint8_t label, std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = GetTotalNumberOfItemsResponseBuilder::MakeBuilder(Status::INVALID_PARAMETER, 0x0000, 0);
send_message(label, true, std::move(response));
return;
}
DEVICE_VLOG(2) << __func__ << ": scope=" << pkt->GetScope();
switch (pkt->GetScope()) {
case Scope::MEDIA_PLAYER_LIST: {
media_interface_->GetMediaPlayerList(
base::Bind(&Device::GetTotalNumberOfItemsMediaPlayersResponse,
weak_ptr_factory_.GetWeakPtr(), label));
break;
}
case Scope::VFS:
media_interface_->GetFolderItems(
curr_browsed_player_id_, CurrentFolder(),
base::Bind(&Device::GetTotalNumberOfItemsVFSResponse,
weak_ptr_factory_.GetWeakPtr(), label));
break;
case Scope::NOW_PLAYING:
media_interface_->GetNowPlayingList(
base::Bind(&Device::GetTotalNumberOfItemsNowPlayingResponse,
weak_ptr_factory_.GetWeakPtr(), label));
break;
default:
DEVICE_LOG(ERROR) << __func__ << ": " << pkt->GetScope();
break;
}
}
void Device::GetTotalNumberOfItemsMediaPlayersResponse(
uint8_t label, uint16_t curr_player, std::vector list) {
DEVICE_VLOG(2) << __func__ << ": num_items=" << list.size();
auto builder = GetTotalNumberOfItemsResponseBuilder::MakeBuilder(
Status::NO_ERROR, 0x0000, list.size());
send_message(label, true, std::move(builder));
}
void Device::GetTotalNumberOfItemsVFSResponse(uint8_t label,
std::vector list) {
DEVICE_VLOG(2) << __func__ << ": num_items=" << list.size();
auto builder = GetTotalNumberOfItemsResponseBuilder::MakeBuilder(
Status::NO_ERROR, 0x0000, list.size());
send_message(label, true, std::move(builder));
}
void Device::GetTotalNumberOfItemsNowPlayingResponse(
uint8_t label, std::string curr_song_id, std::vector list) {
DEVICE_VLOG(2) << __func__ << ": num_items=" << list.size();
auto builder = GetTotalNumberOfItemsResponseBuilder::MakeBuilder(
Status::NO_ERROR, 0x0000, list.size());
send_message(label, true, std::move(builder));
}
void Device::HandleChangePath(uint8_t label,
std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = ChangePathResponseBuilder::MakeBuilder(Status::INVALID_PARAMETER, 0);
send_message(label, true, std::move(response));
return;
}
DEVICE_VLOG(2) << __func__ << ": direction=" << pkt->GetDirection()
<< " uid=" << loghex(pkt->GetUid());
if (pkt->GetDirection() == Direction::DOWN &&
vfs_ids_.get_media_id(pkt->GetUid()) == "") {
DEVICE_LOG(ERROR) << __func__
<< ": No item found for UID=" << pkt->GetUid();
auto builder =
ChangePathResponseBuilder::MakeBuilder(Status::DOES_NOT_EXIST, 0);
send_message(label, true, std::move(builder));
return;
}
if (pkt->GetDirection() == Direction::DOWN) {
current_path_.push(vfs_ids_.get_media_id(pkt->GetUid()));
DEVICE_VLOG(2) << "Pushing Path to stack: \"" << CurrentFolder() << "\"";
} else {
// Don't pop the root id off the stack
if (current_path_.size() > 1) {
current_path_.pop();
} else {
DEVICE_LOG(ERROR) << "Trying to change directory up past root.";
auto builder =
ChangePathResponseBuilder::MakeBuilder(Status::DOES_NOT_EXIST, 0);
send_message(label, true, std::move(builder));
return;
}
DEVICE_VLOG(2) << "Popping Path from stack: new path=\"" << CurrentFolder()
<< "\"";
}
media_interface_->GetFolderItems(
curr_browsed_player_id_, CurrentFolder(),
base::Bind(&Device::ChangePathResponse, weak_ptr_factory_.GetWeakPtr(),
label, pkt));
}
void Device::ChangePathResponse(uint8_t label,
std::shared_ptr pkt,
std::vector list) {
// TODO (apanicke): Reconstruct the VFS ID's here. Right now it gets
// reconstructed in GetFolderItemsVFS
auto builder =
ChangePathResponseBuilder::MakeBuilder(Status::NO_ERROR, list.size());
send_message(label, true, std::move(builder));
}
void Device::HandleGetItemAttributes(
uint8_t label, std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto builder = GetItemAttributesResponseBuilder::MakeBuilder(Status::INVALID_PARAMETER, browse_mtu_);
send_message(label, true, std::move(builder));
return;
}
DEVICE_VLOG(2) << __func__ << ": scope=" << pkt->GetScope()
<< " uid=" << loghex(pkt->GetUid())
<< " uid counter=" << loghex(pkt->GetUidCounter());
if (pkt->GetUidCounter() != 0x0000) { // For database unaware player, use 0
DEVICE_LOG(WARNING) << "UidCounter is invalid";
auto builder = GetItemAttributesResponseBuilder::MakeBuilder(
Status::UIDS_CHANGED, browse_mtu_);
send_message(label, true, std::move(builder));
return;
}
switch (pkt->GetScope()) {
case Scope::NOW_PLAYING: {
media_interface_->GetNowPlayingList(
base::Bind(&Device::GetItemAttributesNowPlayingResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
} break;
case Scope::VFS:
// TODO (apanicke): Check the vfs_ids_ here. If the item doesn't exist
// then we can auto send the error without calling up. We do this check
// later right now though in order to prevent race conditions with updates
// on the media layer.
media_interface_->GetFolderItems(
curr_browsed_player_id_, CurrentFolder(),
base::Bind(&Device::GetItemAttributesVFSResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
break;
default:
DEVICE_LOG(ERROR) << "UNKNOWN SCOPE FOR HANDLE GET ITEM ATTRIBUTES";
break;
}
}
void Device::GetItemAttributesNowPlayingResponse(
uint8_t label, std::shared_ptr pkt,
std::string curr_media_id, std::vector song_list) {
DEVICE_VLOG(2) << __func__ << ": uid=" << loghex(pkt->GetUid());
auto builder = GetItemAttributesResponseBuilder::MakeBuilder(Status::NO_ERROR,
browse_mtu_);
auto media_id = now_playing_ids_.get_media_id(pkt->GetUid());
if (media_id == "") {
media_id = curr_media_id;
}
DEVICE_VLOG(2) << __func__ << ": media_id=\"" << media_id << "\"";
SongInfo info;
if (song_list.size() == 1) {
DEVICE_VLOG(2)
<< __func__
<< " Send out the only song in the queue as now playing song.";
info = song_list.front();
} else {
for (const auto& temp : song_list) {
if (temp.media_id == media_id) {
info = temp;
}
}
}
auto attributes_requested = pkt->GetAttributesRequested();
if (attributes_requested.size() != 0) {
for (const auto& attribute : attributes_requested) {
if (info.attributes.find(attribute) != info.attributes.end()) {
builder->AddAttributeEntry(*info.attributes.find(attribute));
}
}
} else {
// If zero attributes were requested, that means all attributes were
// requested
for (const auto& attribute : info.attributes) {
builder->AddAttributeEntry(attribute);
}
}
send_message(label, true, std::move(builder));
}
void Device::GetItemAttributesVFSResponse(
uint8_t label, std::shared_ptr pkt,
std::vector item_list) {
DEVICE_VLOG(2) << __func__ << ": uid=" << loghex(pkt->GetUid());
auto media_id = vfs_ids_.get_media_id(pkt->GetUid());
if (media_id == "") {
LOG(WARNING) << __func__ << ": Item not found";
auto builder = GetItemAttributesResponseBuilder::MakeBuilder(
Status::DOES_NOT_EXIST, browse_mtu_);
send_message(label, true, std::move(builder));
return;
}
auto builder = GetItemAttributesResponseBuilder::MakeBuilder(Status::NO_ERROR,
browse_mtu_);
ListItem item_requested;
for (const auto& temp : item_list) {
if ((temp.type == ListItem::FOLDER && temp.folder.media_id == media_id) ||
(temp.type == ListItem::SONG && temp.song.media_id == media_id)) {
item_requested = temp;
}
}
// TODO (apanicke): Add a helper function or allow adding a map
// of attributes to GetItemAttributesResponseBuilder
auto attributes_requested = pkt->GetAttributesRequested();
if (item_requested.type == ListItem::FOLDER) {
if (attributes_requested.size() == 0) {
builder->AddAttributeEntry(Attribute::TITLE, item_requested.folder.name);
} else {
for (auto& attr : attributes_requested) {
if (attr == Attribute::TITLE) {
builder->AddAttributeEntry(Attribute::TITLE,
item_requested.folder.name);
}
}
}
} else {
if (attributes_requested.size() != 0) {
for (const auto& attribute : attributes_requested) {
if (item_requested.song.attributes.find(attribute) !=
item_requested.song.attributes.end()) {
builder->AddAttributeEntry(
*item_requested.song.attributes.find(attribute));
}
}
} else {
// If zero attributes were requested, that means all attributes were
// requested
for (const auto& attribute : item_requested.song.attributes) {
builder->AddAttributeEntry(attribute);
}
}
}
send_message(label, true, std::move(builder));
}
void Device::GetMediaPlayerListResponse(
uint8_t label, std::shared_ptr pkt,
uint16_t curr_player, std::vector players) {
DEVICE_VLOG(2) << __func__;
if (players.size() == 0) {
auto no_items_rsp = GetFolderItemsResponseBuilder::MakePlayerListBuilder(
Status::RANGE_OUT_OF_BOUNDS, 0x0000, browse_mtu_);
send_message(label, true, std::move(no_items_rsp));
}
auto builder = GetFolderItemsResponseBuilder::MakePlayerListBuilder(
Status::NO_ERROR, 0x0000, browse_mtu_);
// Move the current player to the first slot due to some carkits always
// connecting to the first listed player rather than using the ID
// returned by Addressed Player Changed
for (auto it = players.begin(); it != players.end(); it++) {
if (it->id == curr_player) {
DEVICE_VLOG(1) << " Adding player to first spot: " << it->name;
auto temp_player = *it;
players.erase(it);
players.insert(players.begin(), temp_player);
break;
}
}
for (size_t i = pkt->GetStartItem();
i <= pkt->GetEndItem() && i < players.size(); i++) {
MediaPlayerItem item(players[i].id, players[i].name,
players[i].browsing_supported);
builder->AddMediaPlayer(item);
}
send_message(label, true, std::move(builder));
}
std::set filter_attributes_requested(
const SongInfo& song, const std::vector& attrs) {
std::set result;
for (const auto& attr : attrs) {
if (song.attributes.find(attr) != song.attributes.end()) {
result.insert(*song.attributes.find(attr));
}
}
return result;
}
void Device::GetVFSListResponse(uint8_t label,
std::shared_ptr pkt,
std::vector items) {
DEVICE_VLOG(2) << __func__ << ": start_item=" << pkt->GetStartItem()
<< " end_item=" << pkt->GetEndItem();
// The builder will automatically correct the status if there are zero items
auto builder = GetFolderItemsResponseBuilder::MakeVFSBuilder(
Status::NO_ERROR, 0x0000, browse_mtu_);
// TODO (apanicke): Add test that checks if vfs_ids_ is the correct size after
// an operation.
for (const auto& item : items) {
if (item.type == ListItem::FOLDER) {
vfs_ids_.insert(item.folder.media_id);
} else if (item.type == ListItem::SONG) {
vfs_ids_.insert(item.song.media_id);
}
}
// Add the elements retrieved in the last get folder items request and map
// them to UIDs The maps will be cleared every time a directory change
// happens. These items do not need to correspond with the now playing list as
// the UID's only need to be unique in the context of the current scope and
// the current folder
for (auto i = pkt->GetStartItem(); i <= pkt->GetEndItem() && i < items.size();
i++) {
if (items[i].type == ListItem::FOLDER) {
auto folder = items[i].folder;
// right now we always use folders of mixed type
FolderItem folder_item(vfs_ids_.get_uid(folder.media_id), 0x00,
folder.is_playable, folder.name);
if (!builder->AddFolder(folder_item)) break;
} else if (items[i].type == ListItem::SONG) {
auto song = items[i].song;
auto title =
song.attributes.find(Attribute::TITLE) != song.attributes.end()
? song.attributes.find(Attribute::TITLE)->value()
: "No Song Info";
MediaElementItem song_item(vfs_ids_.get_uid(song.media_id), title,
std::set());
if (pkt->GetNumAttributes() == 0x00) { // All attributes requested
song_item.attributes_ = std::move(song.attributes);
} else {
song_item.attributes_ =
filter_attributes_requested(song, pkt->GetAttributesRequested());
}
// If we fail to add a song, don't accidentally add one later that might
// fit.
if (!builder->AddSong(song_item)) break;
}
}
send_message(label, true, std::move(builder));
}
void Device::GetNowPlayingListResponse(
uint8_t label, std::shared_ptr pkt,
std::string /* unused curr_song_id */, std::vector song_list) {
DEVICE_VLOG(2) << __func__;
auto builder = GetFolderItemsResponseBuilder::MakeNowPlayingBuilder(
Status::NO_ERROR, 0x0000, browse_mtu_);
now_playing_ids_.clear();
for (const SongInfo& song : song_list) {
now_playing_ids_.insert(song.media_id);
}
for (size_t i = pkt->GetStartItem();
i <= pkt->GetEndItem() && i < song_list.size(); i++) {
auto song = song_list[i];
auto title = song.attributes.find(Attribute::TITLE) != song.attributes.end()
? song.attributes.find(Attribute::TITLE)->value()
: "No Song Info";
MediaElementItem item(i + 1, title, std::set());
if (pkt->GetNumAttributes() == 0x00) {
item.attributes_ = std::move(song.attributes);
} else {
item.attributes_ =
filter_attributes_requested(song, pkt->GetAttributesRequested());
}
// If we fail to add a song, don't accidentally add one later that might
// fit.
if (!builder->AddSong(item)) break;
}
send_message(label, true, std::move(builder));
}
void Device::HandleSetBrowsedPlayer(
uint8_t label, std::shared_ptr pkt) {
if (!pkt->IsValid()) {
DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid";
auto response = SetBrowsedPlayerResponseBuilder::MakeBuilder(Status::INVALID_PARAMETER, 0x0000, 0, 0, "");
send_message(label, true, std::move(response));
return;
}
DEVICE_VLOG(2) << __func__ << ": player_id=" << pkt->GetPlayerId();
media_interface_->SetBrowsedPlayer(
pkt->GetPlayerId(),
base::Bind(&Device::SetBrowsedPlayerResponse,
weak_ptr_factory_.GetWeakPtr(), label, pkt));
}
void Device::SetBrowsedPlayerResponse(
uint8_t label, std::shared_ptr pkt, bool success,
std::string root_id, uint32_t num_items) {
DEVICE_VLOG(2) << __func__ << ": success=" << success << " root_id=\""
<< root_id << "\" num_items=" << num_items;
if (!success) {
auto response = SetBrowsedPlayerResponseBuilder::MakeBuilder(
Status::INVALID_PLAYER_ID, 0x0000, num_items, 0, "");
send_message(label, true, std::move(response));
return;
}
curr_browsed_player_id_ = pkt->GetPlayerId();
// Clear the path and push the new root.
current_path_ = std::stack();
current_path_.push(root_id);
auto response = SetBrowsedPlayerResponseBuilder::MakeBuilder(
Status::NO_ERROR, 0x0000, num_items, 0, "");
send_message(label, true, std::move(response));
}
void Device::SendMediaUpdate(bool metadata, bool play_status, bool queue) {
bool is_silence = IsInSilenceMode();
CHECK(media_interface_);
DEVICE_VLOG(4) << __func__ << ": Metadata=" << metadata
<< " : play_status= " << play_status << " : queue=" << queue
<< " ; is_silence=" << is_silence;
if (queue) {
HandleNowPlayingUpdate();
}
if (play_status) {
HandlePlayStatusUpdate();
if (!is_silence) {
HandlePlayPosUpdate();
}
}
if (metadata) HandleTrackUpdate();
}
void Device::SendFolderUpdate(bool available_players, bool addressed_player,
bool uids) {
CHECK(media_interface_);
DEVICE_VLOG(4) << __func__;
if (available_players) {
HandleAvailablePlayerUpdate();
}
if (addressed_player) {
HandleAddressedPlayerUpdate();
}
}
void Device::HandleTrackUpdate() {
DEVICE_VLOG(2) << __func__;
if (!track_changed_.first) {
LOG(WARNING) << "Device is not registered for track changed updates";
return;
}
media_interface_->GetNowPlayingList(
base::Bind(&Device::TrackChangedNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), track_changed_.second, false));
}
void Device::HandlePlayStatusUpdate() {
DEVICE_VLOG(2) << __func__;
if (!play_status_changed_.first) {
LOG(WARNING) << "Device is not registered for play status updates";
return;
}
media_interface_->GetPlayStatus(base::Bind(
&Device::PlaybackStatusNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), play_status_changed_.second, false));
}
void Device::HandleNowPlayingUpdate() {
DEVICE_VLOG(2) << __func__;
if (!now_playing_changed_.first) {
LOG(WARNING) << "Device is not registered for now playing updates";
return;
}
media_interface_->GetNowPlayingList(base::Bind(
&Device::HandleNowPlayingNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), now_playing_changed_.second, false));
}
void Device::HandleNowPlayingNotificationResponse(
uint8_t label, bool interim, std::string curr_song_id,
std::vector song_list) {
if (interim) {
now_playing_changed_ = Notification(true, label);
} else if (!now_playing_changed_.first) {
LOG(WARNING) << "Device is not registered for now playing updates";
return;
}
now_playing_ids_.clear();
for (const SongInfo& song : song_list) {
now_playing_ids_.insert(song.media_id);
}
auto response =
RegisterNotificationResponseBuilder::MakeNowPlayingBuilder(interim);
send_message(now_playing_changed_.second, false, std::move(response));
if (!interim) {
active_labels_.erase(label);
now_playing_changed_ = Notification(false, 0);
}
}
void Device::HandlePlayPosUpdate() {
DEVICE_VLOG(0) << __func__;
if (!play_pos_changed_.first) {
LOG(WARNING) << "Device is not registered for play position updates";
return;
}
media_interface_->GetPlayStatus(base::Bind(
&Device::PlaybackPosNotificationResponse, weak_ptr_factory_.GetWeakPtr(),
play_pos_changed_.second, false));
}
void Device::HandleAvailablePlayerUpdate() {
DEVICE_VLOG(1) << __func__;
if (!avail_players_changed_.first) {
LOG(WARNING) << "Device is not registered for available player updates";
return;
}
auto response =
RegisterNotificationResponseBuilder::MakeAvailablePlayersBuilder(false);
send_message_cb_.Run(avail_players_changed_.second, false,
std::move(response));
if (!avail_players_changed_.first) {
active_labels_.erase(avail_players_changed_.second);
avail_players_changed_ = Notification(false, 0);
}
}
void Device::HandleAddressedPlayerUpdate() {
DEVICE_VLOG(1) << __func__;
if (!addr_player_changed_.first) {
DEVICE_LOG(WARNING)
<< "Device is not registered for addressed player updates";
return;
}
media_interface_->GetMediaPlayerList(base::Bind(
&Device::AddressedPlayerNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), addr_player_changed_.second, false));
}
void Device::DeviceDisconnected() {
DEVICE_LOG(INFO) << "Device was disconnected";
play_pos_update_cb_.Cancel();
// TODO (apanicke): Once the interfaces are set in the Device construction,
// remove these conditionals.
if (volume_interface_ != nullptr)
volume_interface_->DeviceDisconnected(GetAddress());
}
static std::string volumeToStr(int8_t volume) {
if (volume == VOL_NOT_SUPPORTED) return "Absolute Volume not supported";
if (volume == VOL_REGISTRATION_FAILED)
return "Volume changed notification was rejected";
return std::to_string(volume);
}
std::ostream& operator<<(std::ostream& out, const Device& d) {
out << d.address_.ToString();
if (d.IsActive()) out << " ";
out << std::endl;
ScopedIndent indent(out);
out << "Current Volume: " << volumeToStr(d.volume_) << std::endl;
out << "Current Browsed Player ID: " << d.curr_browsed_player_id_
<< std::endl;
out << "Registered Notifications:\n";
{
ScopedIndent indent(out);
if (d.track_changed_.first) out << "Track Changed\n";
if (d.play_status_changed_.first) out << "Play Status\n";
if (d.play_pos_changed_.first) out << "Play Position\n";
if (d.now_playing_changed_.first) out << "Now Playing\n";
if (d.addr_player_changed_.first) out << "Addressed Player\n";
if (d.avail_players_changed_.first) out << "Available Players\n";
if (d.uids_changed_.first) out << "UIDs Changed\n";
}
out << "Last Play State: " << d.last_play_status_.state << std::endl;
out << "Last Song Sent ID: \"" << d.last_song_info_.media_id << "\"\n";
out << "Current Folder: \"" << d.CurrentFolder() << "\"\n";
out << "MTU Sizes: CTRL=" << d.ctrl_mtu_ << " BROWSE=" << d.browse_mtu_
<< std::endl;
// TODO (apanicke): Add supported features as well as media keys
return out;
}
} // namespace avrcp
} // namespace bluetooth