/* * 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 <base/message_loop/message_loop.h> #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<void(uint8_t label, bool browse, std::unique_ptr<::bluetooth::PacketBuilder> 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> 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<VendorPacket> 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<CommandPdu>(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<RegisterNotificationResponse>(pkt); 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<GetCapabilitiesRequest>(pkt)); } break; case CommandPdu::REGISTER_NOTIFICATION: { HandleNotification(label, Packet::Specialize<RegisterNotificationRequest>(pkt)); } break; case CommandPdu::GET_ELEMENT_ATTRIBUTES: { auto get_element_attributes_request_pkt = Packet::Specialize<GetElementAttributesRequest>(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<PlayItemRequest>(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<SetAddressedPlayerRequest>(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<GetCapabilitiesRequest>& 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<RegisterNotificationRequest>& 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<RegisterNotificationResponse>& pkt) { DEVICE_VLOG(1) << __func__ << ": interim=" << pkt->IsInterim(); 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)); active_labels_.erase(label); volume_interface_ = nullptr; volume_ = VOL_REGISTRATION_FAILED; return; } 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<SongInfo> 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<MediaPlayerInfo> /* 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<GetElementAttributesRequest> 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<Packet> pkt) { if (!pkt->IsValid()) { DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid"; auto response = RejectBuilder::MakeBuilder(static_cast<CommandPdu>(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<PassThroughPacket>(pkt); if (!pass_through_packet->IsValid()) { DEVICE_LOG(WARNING) << __func__ << ": Request packet is not valid"; auto response = RejectBuilder::MakeBuilder(static_cast<CommandPdu>(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<Device> 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<VendorPacket>(pkt); VendorPacketHandler(label, vendor_pkt); } break; } } void Device::HandlePlayItem(uint8_t label, std::shared_ptr<PlayItemRequest> 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<SetAddressedPlayerRequest> pkt, uint16_t curr_player, std::vector<MediaPlayerInfo> 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<BrowsePacket> 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<SetBrowsedPlayerRequest>(pkt)); break; case BrowsePdu::GET_FOLDER_ITEMS: HandleGetFolderItems(label, Packet::Specialize<GetFolderItemsRequest>(pkt)); break; case BrowsePdu::CHANGE_PATH: HandleChangePath(label, Packet::Specialize<ChangePathRequest>(pkt)); break; case BrowsePdu::GET_ITEM_ATTRIBUTES: HandleGetItemAttributes( label, Packet::Specialize<GetItemAttributesRequest>(pkt)); break; case BrowsePdu::GET_TOTAL_NUMBER_OF_ITEMS: HandleGetTotalNumberOfItems( label, Packet::Specialize<GetTotalNumberOfItemsRequest>(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<GetFolderItemsRequest> 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<GetTotalNumberOfItemsRequest> 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<MediaPlayerInfo> 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<ListItem> 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<SongInfo> 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<ChangePathRequest> 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<ChangePathRequest> pkt, std::vector<ListItem> 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<GetItemAttributesRequest> 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<GetItemAttributesRequest> pkt, std::string curr_media_id, std::vector<SongInfo> 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; 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<GetItemAttributesRequest> pkt, std::vector<ListItem> 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<GetFolderItemsRequest> pkt, uint16_t curr_player, std::vector<MediaPlayerInfo> 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<AttributeEntry> filter_attributes_requested( const SongInfo& song, const std::vector<Attribute>& attrs) { std::set<AttributeEntry> 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<GetFolderItemsRequest> pkt, std::vector<ListItem> 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<AttributeEntry>()); 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<GetFolderItemsRequest> pkt, std::string /* unused curr_song_id */, std::vector<SongInfo> 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<AttributeEntry>()); 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<SetBrowsedPlayerRequest> 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<SetBrowsedPlayerRequest> 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<std::string>(); 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<SongInfo> 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 << " <Active>"; 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