diff -r 000000000000 -r 6474c204b198 dom/bluetooth/bluez/BluetoothA2dpManager.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/bluetooth/bluez/BluetoothA2dpManager.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,477 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/basictypes.h" + +#include "BluetoothA2dpManager.h" + +#include "BluetoothCommon.h" +#include "BluetoothService.h" +#include "BluetoothSocket.h" +#include "BluetoothUtils.h" + +#include "mozilla/dom/bluetooth/BluetoothTypes.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsIObserverService.h" +#include "MainThreadUtils.h" + + +using namespace mozilla; +USING_BLUETOOTH_NAMESPACE + +namespace { + StaticRefPtr sBluetoothA2dpManager; + bool sInShutdown = false; +} // anonymous namespace + +NS_IMETHODIMP +BluetoothA2dpManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(sBluetoothA2dpManager); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + HandleShutdown(); + return NS_OK; + } + + MOZ_ASSERT(false, "BluetoothA2dpManager got unexpected topic!"); + return NS_ERROR_UNEXPECTED; +} + +BluetoothA2dpManager::BluetoothA2dpManager() +{ + Reset(); +} + +void +BluetoothA2dpManager::Reset() +{ + ResetA2dp(); + ResetAvrcp(); +} + +bool +BluetoothA2dpManager::Init() +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, false); + if (NS_FAILED(obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false))) { + BT_WARNING("Failed to add shutdown observer!"); + return false; + } + + return true; +} + +BluetoothA2dpManager::~BluetoothA2dpManager() +{ + nsCOMPtr obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + if (NS_FAILED(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID))) { + BT_WARNING("Failed to remove shutdown observer!"); + } +} + +void +BluetoothA2dpManager::ResetA2dp() +{ + mA2dpConnected = false; + mSinkState = SinkState::SINK_DISCONNECTED; + mController = nullptr; +} + +void +BluetoothA2dpManager::ResetAvrcp() +{ + mAvrcpConnected = false; + mDuration = 0; + mMediaNumber = 0; + mTotalMediaCount = 0; + mPosition = 0; + mPlayStatus = ControlPlayStatus::PLAYSTATUS_UNKNOWN; +} + +static BluetoothA2dpManager::SinkState +StatusStringToSinkState(const nsAString& aStatus) +{ + BluetoothA2dpManager::SinkState state = + BluetoothA2dpManager::SinkState::SINK_UNKNOWN; + if (aStatus.EqualsLiteral("disconnected")) { + state = BluetoothA2dpManager::SinkState::SINK_DISCONNECTED; + } else if (aStatus.EqualsLiteral("connecting")) { + state = BluetoothA2dpManager::SinkState::SINK_CONNECTING; + } else if (aStatus.EqualsLiteral("connected")) { + state = BluetoothA2dpManager::SinkState::SINK_CONNECTED; + } else if (aStatus.EqualsLiteral("playing")) { + state = BluetoothA2dpManager::SinkState::SINK_PLAYING; + } else { + BT_WARNING("Unknown sink state"); + } + return state; +} + +//static +BluetoothA2dpManager* +BluetoothA2dpManager::Get() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // If sBluetoothA2dpManager already exists, exit early + if (sBluetoothA2dpManager) { + return sBluetoothA2dpManager; + } + + // If we're in shutdown, don't create a new instance + NS_ENSURE_FALSE(sInShutdown, nullptr); + + // Create a new instance, register, and return + BluetoothA2dpManager* manager = new BluetoothA2dpManager(); + NS_ENSURE_TRUE(manager->Init(), nullptr); + + sBluetoothA2dpManager = manager; + return sBluetoothA2dpManager; +} + +void +BluetoothA2dpManager::HandleShutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + sInShutdown = true; + Disconnect(nullptr); + sBluetoothA2dpManager = nullptr; +} + +void +BluetoothA2dpManager::Connect(const nsAString& aDeviceAddress, + BluetoothProfileController* aController) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aDeviceAddress.IsEmpty()); + MOZ_ASSERT(aController && !mController); + + BluetoothService* bs = BluetoothService::Get(); + if (!bs || sInShutdown) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); + return; + } + + if (mA2dpConnected) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_ALREADY_CONNECTED)); + return; + } + + mDeviceAddress = aDeviceAddress; + mController = aController; + + if (NS_FAILED(bs->SendSinkMessage(aDeviceAddress, + NS_LITERAL_STRING("Connect")))) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); + return; + } +} + +void +BluetoothA2dpManager::Disconnect(BluetoothProfileController* aController) +{ + BluetoothService* bs = BluetoothService::Get(); + if (!bs) { + if (aController) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); + } + return; + } + + if (!mA2dpConnected) { + if (aController) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_ALREADY_DISCONNECTED)); + } + return; + } + + MOZ_ASSERT(!mDeviceAddress.IsEmpty()); + MOZ_ASSERT(!mController); + + mController = aController; + + if (NS_FAILED(bs->SendSinkMessage(mDeviceAddress, + NS_LITERAL_STRING("Disconnect")))) { + aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); + return; + } +} + +void +BluetoothA2dpManager::OnConnect(const nsAString& aErrorStr) +{ + MOZ_ASSERT(NS_IsMainThread()); + + /** + * On the one hand, notify the controller that we've done for outbound + * connections. On the other hand, we do nothing for inbound connections. + */ + NS_ENSURE_TRUE_VOID(mController); + + nsRefPtr controller = mController.forget(); + controller->NotifyCompletion(aErrorStr); +} + +void +BluetoothA2dpManager::OnDisconnect(const nsAString& aErrorStr) +{ + MOZ_ASSERT(NS_IsMainThread()); + + /** + * On the one hand, notify the controller that we've done for outbound + * connections. On the other hand, we do nothing for inbound connections. + */ + NS_ENSURE_TRUE_VOID(mController); + + nsRefPtr controller = mController.forget(); + controller->NotifyCompletion(aErrorStr); + + Reset(); +} + +/* HandleSinkPropertyChanged update sink state in A2dp + * + * Possible values: "disconnected", "connecting", "connected", "playing" + * + * 1. "disconnected" -> "connecting" + * Either an incoming or outgoing connection attempt ongoing + * 2. "connecting" -> "disconnected" + * Connection attempt failed + * 3. "connecting" -> "connected" + * Successfully connected + * 4. "connected" -> "playing" + * Audio stream active + * 5. "playing" -> "connected" + * Audio stream suspended + * 6. "connected" -> "disconnected" + * "playing" -> "disconnected" + * Disconnected from local or the remote device + */ +void +BluetoothA2dpManager::HandleSinkPropertyChanged(const BluetoothSignal& aSignal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aSignal.value().type() == BluetoothValue::TArrayOfBluetoothNamedValue); + + const nsString& address = aSignal.path(); + /** + * Update sink property only if + * - mDeviceAddress is empty (A2dp is disconnected), or + * - this property change is from the connected sink. + */ + NS_ENSURE_TRUE_VOID(mDeviceAddress.IsEmpty() || + mDeviceAddress.Equals(address)); + + const InfallibleTArray& arr = + aSignal.value().get_ArrayOfBluetoothNamedValue(); + MOZ_ASSERT(arr.Length() == 1); + + /** + * There are three properties: + * - "State": a string + * - "Connected": a boolean value + * - "Playing": a boolean value + * + * Note that only "State" is handled in this function. + */ + + const nsString& name = arr[0].name(); + NS_ENSURE_TRUE_VOID(name.EqualsLiteral("State")); + + const BluetoothValue& value = arr[0].value(); + MOZ_ASSERT(value.type() == BluetoothValue::TnsString); + SinkState newState = StatusStringToSinkState(value.get_nsString()); + NS_ENSURE_TRUE_VOID((newState != SinkState::SINK_UNKNOWN) && + (newState != mSinkState)); + + /** + * Reject 'connected' state change if bluetooth is already disabled. + * Sink state would be reset to 'disconnected' when bluetooth is disabled. + * + * See bug 984284 for more information about the edge case. + */ + NS_ENSURE_FALSE_VOID(newState == SinkState::SINK_CONNECTED && + mSinkState == SinkState::SINK_DISCONNECTED); + + SinkState prevState = mSinkState; + mSinkState = newState; + + switch(mSinkState) { + case SinkState::SINK_CONNECTING: + // case 1: Either an incoming or outgoing connection attempt ongoing + MOZ_ASSERT(prevState == SinkState::SINK_DISCONNECTED); + break; + case SinkState::SINK_PLAYING: + // case 4: Audio stream active + MOZ_ASSERT(prevState == SinkState::SINK_CONNECTED); + break; + case SinkState::SINK_CONNECTED: + // case 5: Audio stream suspended + if (prevState == SinkState::SINK_PLAYING) { + break; + } + + // case 3: Successfully connected + MOZ_ASSERT(prevState == SinkState::SINK_CONNECTING); + + mA2dpConnected = true; + mDeviceAddress = address; + NotifyConnectionStatusChanged(); + + OnConnect(EmptyString()); + break; + case SinkState::SINK_DISCONNECTED: + // case 2: Connection attempt failed + if (prevState == SinkState::SINK_CONNECTING) { + OnConnect(NS_LITERAL_STRING(ERR_CONNECTION_FAILED)); + break; + } + + // case 6: Disconnected from the remote device + MOZ_ASSERT(prevState == SinkState::SINK_CONNECTED || + prevState == SinkState::SINK_PLAYING); + + mA2dpConnected = false; + NotifyConnectionStatusChanged(); + mDeviceAddress.Truncate(); + OnDisconnect(EmptyString()); + break; + default: + break; + } +} + +void +BluetoothA2dpManager::NotifyConnectionStatusChanged() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Notify Gecko observers + nsCOMPtr obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + + if (NS_FAILED(obs->NotifyObservers(this, + BLUETOOTH_A2DP_STATUS_CHANGED_ID, + mDeviceAddress.get()))) { + BT_WARNING("Failed to notify bluetooth-a2dp-status-changed observsers!"); + } + + // Dispatch an event of status change + DispatchStatusChangedEvent( + NS_LITERAL_STRING(A2DP_STATUS_CHANGED_ID), mDeviceAddress, mA2dpConnected); +} + +void +BluetoothA2dpManager::OnGetServiceChannel(const nsAString& aDeviceAddress, + const nsAString& aServiceUuid, + int aChannel) +{ +} + +void +BluetoothA2dpManager::OnUpdateSdpRecords(const nsAString& aDeviceAddress) +{ +} + +void +BluetoothA2dpManager::GetAddress(nsAString& aDeviceAddress) +{ + aDeviceAddress = mDeviceAddress; +} + +bool +BluetoothA2dpManager::IsConnected() +{ + return mA2dpConnected; +} + +void +BluetoothA2dpManager::SetAvrcpConnected(bool aConnected) +{ + mAvrcpConnected = aConnected; + if (!aConnected) { + ResetAvrcp(); + } +} + +bool +BluetoothA2dpManager::IsAvrcpConnected() +{ + return mAvrcpConnected; +} + +void +BluetoothA2dpManager::UpdateMetaData(const nsAString& aTitle, + const nsAString& aArtist, + const nsAString& aAlbum, + uint64_t aMediaNumber, + uint64_t aTotalMediaCount, + uint32_t aDuration) +{ + mTitle.Assign(aTitle); + mArtist.Assign(aArtist); + mAlbum.Assign(aAlbum); + mMediaNumber = aMediaNumber; + mTotalMediaCount = aTotalMediaCount; + mDuration = aDuration; +} + +void +BluetoothA2dpManager::UpdatePlayStatus(uint32_t aDuration, + uint32_t aPosition, + ControlPlayStatus aPlayStatus) +{ + mDuration = aDuration; + mPosition = aPosition; + mPlayStatus = aPlayStatus; +} + +void +BluetoothA2dpManager::GetAlbum(nsAString& aAlbum) +{ + aAlbum.Assign(mAlbum); +} + +uint32_t +BluetoothA2dpManager::GetDuration() +{ + return mDuration; +} + +ControlPlayStatus +BluetoothA2dpManager::GetPlayStatus() +{ + return mPlayStatus; +} + +uint32_t +BluetoothA2dpManager::GetPosition() +{ + return mPosition; +} + +uint64_t +BluetoothA2dpManager::GetMediaNumber() +{ + return mMediaNumber; +} + +void +BluetoothA2dpManager::GetTitle(nsAString& aTitle) +{ + aTitle.Assign(mTitle); +} + +NS_IMPL_ISUPPORTS(BluetoothA2dpManager, nsIObserver) +