diff options
Diffstat (limited to 'dom/system/gonk/AudioManager.cpp')
-rw-r--r-- | dom/system/gonk/AudioManager.cpp | 1412 |
1 files changed, 1412 insertions, 0 deletions
diff --git a/dom/system/gonk/AudioManager.cpp b/dom/system/gonk/AudioManager.cpp new file mode 100644 index 000000000..88dff13f7 --- /dev/null +++ b/dom/system/gonk/AudioManager.cpp @@ -0,0 +1,1412 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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 <android/log.h> +#include <cutils/properties.h> +#include <binder/IServiceManager.h> + +#include "AudioChannelService.h" +#include "AudioManager.h" + +#include "nsIObserverService.h" +#include "nsISettingsService.h" +#include "nsPrintfCString.h" + +#include "mozilla/Hal.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/MozPromise.h" +#include "mozilla/dom/ScriptSettings.h" +#include "base/message_loop.h" + +#include "BluetoothCommon.h" +#include "BluetoothHfpManagerBase.h" + +#include "nsJSUtils.h" +#include "nsThreadUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::gonk; +using namespace android; +using namespace mozilla; +using namespace mozilla::dom::bluetooth; + +#undef LOG +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "AudioManager" , ## args) + +#define HEADPHONES_STATUS_HEADSET u"headset" +#define HEADPHONES_STATUS_HEADPHONE u"headphone" +#define HEADPHONES_STATUS_OFF u"off" +#define HEADPHONES_STATUS_UNKNOWN u"unknown" +#define HEADPHONES_STATUS_CHANGED "headphones-status-changed" +#define MOZ_SETTINGS_CHANGE_ID "mozsettings-changed" +#define AUDIO_CHANNEL_PROCESS_CHANGED "audio-channel-process-changed" +#define AUDIO_POLICY_SERVICE_NAME "media.audio_policy" +#define SETTINGS_SERVICE "@mozilla.org/settingsService;1" + +// Refer AudioService.java from Android +static const uint32_t sMaxStreamVolumeTbl[AUDIO_STREAM_CNT] = { + 5, // voice call + 15, // system + 15, // ring + 15, // music + 15, // alarm + 15, // notification + 15, // BT SCO + 15, // enforced audible + 15, // DTMF + 15, // TTS +#if ANDROID_VERSION < 19 + 15, // FM +#endif +}; + +static const uint32_t sDefaultStreamVolumeTbl[AUDIO_STREAM_CNT] = { + 3, // voice call + 8, // system + 8, // ring + 8, // music + 8, // alarm + 8, // notification + 8, // BT SCO + 15, // enforced audible // XXX Handle as fixed maximum audio setting + 8, // DTMF + 8, // TTS +#if ANDROID_VERSION < 19 + 8, // FM +#endif +}; + +static const int32_t sStreamVolumeAliasTbl[AUDIO_STREAM_CNT] = { + AUDIO_STREAM_VOICE_CALL, // voice call + AUDIO_STREAM_NOTIFICATION, // system + AUDIO_STREAM_NOTIFICATION, // ring + AUDIO_STREAM_MUSIC, // music + AUDIO_STREAM_ALARM, // alarm + AUDIO_STREAM_NOTIFICATION, // notification + AUDIO_STREAM_BLUETOOTH_SCO, // BT SCO + AUDIO_STREAM_ENFORCED_AUDIBLE,// enforced audible + AUDIO_STREAM_DTMF, // DTMF + AUDIO_STREAM_TTS, // TTS +#if ANDROID_VERSION < 19 + AUDIO_STREAM_MUSIC, // FM +#endif +}; + +static const uint32_t sChannelStreamTbl[NUMBER_OF_AUDIO_CHANNELS] = { + AUDIO_STREAM_MUSIC, // AudioChannel::Normal + AUDIO_STREAM_MUSIC, // AudioChannel::Content + AUDIO_STREAM_NOTIFICATION, // AudioChannel::Notification + AUDIO_STREAM_ALARM, // AudioChannel::Alarm + AUDIO_STREAM_VOICE_CALL, // AudioChannel::Telephony + AUDIO_STREAM_RING, // AudioChannel::Ringer + AUDIO_STREAM_ENFORCED_AUDIBLE,// AudioChannel::Publicnotification + AUDIO_STREAM_SYSTEM, // AudioChannel::System +}; + + +struct AudioDeviceInfo { + /** The string the value maps to */ + const char* tag; + /** The enum value that maps to this string */ + uint32_t value; +}; + +// Mappings audio output devices to strings. +static const AudioDeviceInfo kAudioDeviceInfos[] = { + { "earpiece", AUDIO_DEVICE_OUT_EARPIECE }, + { "speaker", AUDIO_DEVICE_OUT_SPEAKER }, + { "wired_headset", AUDIO_DEVICE_OUT_WIRED_HEADSET }, + { "wired_headphone", AUDIO_DEVICE_OUT_WIRED_HEADPHONE }, + { "bt_scoheadset", AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET }, + { "bt_a2dp", AUDIO_DEVICE_OUT_BLUETOOTH_A2DP }, +}; + +static const int kBtSampleRate = 8000; + +typedef MozPromise<bool, const char*, true> VolumeInitPromise; + +namespace mozilla { +namespace dom { +namespace gonk { + +/** + * We have five sound volume settings from UX spec, + * You can see more informations in Bug1068219. + * (1) Media : music, video, FM ... + * (2) Notification : ringer, notification ... + * (3) Alarm : alarm + * (4) Telephony : GSM call, WebRTC call + * (5) Bluetooth SCO : SCO call + **/ +struct VolumeData { + const char* mChannelName; + int32_t mStreamType; +}; + +static const VolumeData gVolumeData[] = { + {"audio.volume.content", AUDIO_STREAM_MUSIC}, + {"audio.volume.notification", AUDIO_STREAM_NOTIFICATION}, + {"audio.volume.alarm", AUDIO_STREAM_ALARM}, + {"audio.volume.telephony", AUDIO_STREAM_VOICE_CALL}, + {"audio.volume.bt_sco", AUDIO_STREAM_BLUETOOTH_SCO} +}; + +class RunnableCallTask : public Runnable +{ +public: + explicit RunnableCallTask(nsIRunnable* aRunnable) + : mRunnable(aRunnable) {} + + NS_IMETHOD Run() override + { + return mRunnable->Run(); + } +protected: + nsCOMPtr<nsIRunnable> mRunnable; +}; + +nsCOMPtr<nsISettingsServiceLock> +GetSettingServiceLock() +{ + nsresult rv; + nsCOMPtr<nsISettingsService> service = do_GetService(SETTINGS_SERVICE, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsISettingsServiceLock> lock; + rv = service->CreateLock(nullptr, getter_AddRefs(lock)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return lock.forget(); +} + +#if ANDROID_VERSION >= 21 +class GonkAudioPortCallback : public AudioSystem::AudioPortCallback +{ +public: + virtual void onAudioPortListUpdate() + { + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([]() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + NS_ENSURE_TRUE(audioManager.get(), ); + audioManager->UpdateCachedActiveDevicesForStreams(); + audioManager->MaybeUpdateVolumeSettingToDatabase(); + }); + NS_DispatchToMainThread(runnable); + } + virtual void onAudioPatchListUpdate() { } + virtual void onServiceDied() { } +}; +#endif + +void +AudioManager::HandleAudioFlingerDied() +{ + //Disable volume change notification + mIsVolumeInited = false; + + uint32_t attempt; + for (attempt = 0; attempt < 50; attempt++) { + if (defaultServiceManager()->checkService(String16(AUDIO_POLICY_SERVICE_NAME)) != 0) { + break; + } + LOG("AudioPolicyService is dead! attempt=%d", attempt); + usleep(1000 * 200); + } + + MOZ_RELEASE_ASSERT(attempt < 50); + + // Indicate to audio HAL that we start the reconfiguration phase after a media + // server crash + AudioSystem::setParameters(0, String8("restarting=true")); + + // Restore device connection states + SetAllDeviceConnectionStates(); + + // Restore call state +#if ANDROID_VERSION < 17 + AudioSystem::setPhoneState(mPhoneState); +#else + AudioSystem::setPhoneState(static_cast<audio_mode_t>(mPhoneState)); +#endif + + // Restore master volume + AudioSystem::setMasterVolume(1.0); + + // Restore stream volumes + for (uint32_t streamType = 0; streamType < AUDIO_STREAM_CNT; ++streamType) { + mStreamStates[streamType]->InitStreamVolume(); + mStreamStates[streamType]->RestoreVolumeIndexToAllDevices(); + } + + // Indicate the end of reconfiguration phase to audio HAL + AudioSystem::setParameters(0, String8("restarting=true")); + + // Enable volume change notification + mIsVolumeInited = true; + mAudioOutDevicesUpdated = 0; + MaybeUpdateVolumeSettingToDatabase(true); +} + +class VolumeInitCallback final : public nsISettingsServiceCallback +{ +public: + NS_DECL_ISUPPORTS + + VolumeInitCallback() + : mInitCounter(0) + { + mPromise = mPromiseHolder.Ensure(__func__); + } + + RefPtr<VolumeInitPromise> GetPromise() const + { + return mPromise; + } + + NS_IMETHOD Handle(const nsAString& aName, JS::Handle<JS::Value> aResult) + { + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + MOZ_ASSERT(audioManager); + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + NS_ConvertASCIItoUTF16 volumeType(gVolumeData[idx].mChannelName); + if (StringBeginsWith(aName, volumeType)) { + uint32_t device = GetDeviceFromSettingName(aName); + MOZ_ASSERT(device != AUDIO_DEVICE_NONE); + if (aResult.isInt32()) { + int32_t stream = gVolumeData[idx].mStreamType; + uint32_t volIndex = aResult.toInt32(); + nsresult rv = audioManager->ValidateVolumeIndex(stream, volIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromiseHolder.Reject("Error : invalid volume index.", __func__); + return rv; + } + audioManager->SetStreamVolumeForDevice(stream, volIndex, device); + } + + if (++mInitCounter == MOZ_ARRAY_LENGTH(kAudioDeviceInfos) * MOZ_ARRAY_LENGTH(gVolumeData)) { + mPromiseHolder.Resolve(true, __func__); + } + return NS_OK; + } + } + mPromiseHolder.Reject("Error : unexpected audio init event.", __func__); + return NS_OK; + } + + NS_IMETHOD HandleError(const nsAString& aName) + { + mPromiseHolder.Reject(NS_ConvertUTF16toUTF8(aName).get(), __func__); + return NS_OK; + } + +protected: + ~VolumeInitCallback() {} + + uint32_t GetDeviceFromSettingName(const nsAString& aName) const + { + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx) { + NS_ConvertASCIItoUTF16 device(kAudioDeviceInfos[idx].tag); + if (StringEndsWith(aName, device)) { + return kAudioDeviceInfos[idx].value; + } + } + return AUDIO_DEVICE_NONE; + } + + RefPtr<VolumeInitPromise> mPromise; + MozPromiseHolder<VolumeInitPromise> mPromiseHolder; + uint32_t mInitCounter; +}; + +NS_IMPL_ISUPPORTS(VolumeInitCallback, nsISettingsServiceCallback) + +static void +BinderDeadCallback(status_t aErr) +{ + if (aErr != DEAD_OBJECT) { + return; + } + + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([]() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + NS_ENSURE_TRUE(audioManager.get(), ); + audioManager->HandleAudioFlingerDied(); + }); + + NS_DispatchToMainThread(runnable); +} + +bool +AudioManager::IsFmOutConnected() +{ + return mConnectedDevices.Get(AUDIO_DEVICE_OUT_FM, nullptr); +} + +NS_IMPL_ISUPPORTS(AudioManager, nsIAudioManager, nsIObserver) + +void +AudioManager::AudioOutDeviceUpdated(uint32_t aDevice) +{ + MOZ_ASSERT(audio_is_output_device(aDevice)); + mAudioOutDevicesUpdated |= aDevice; +} + +void +AudioManager::UpdateHeadsetConnectionState(hal::SwitchState aState) +{ + bool headphoneConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + nullptr); + bool headsetConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADSET, + nullptr); + if (aState == hal::SWITCH_STATE_HEADSET) { + UpdateDeviceConnectionState(true, + AUDIO_DEVICE_OUT_WIRED_HEADSET, + NS_LITERAL_CSTRING("")); + } else if (aState == hal::SWITCH_STATE_HEADPHONE) { + UpdateDeviceConnectionState(true, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + NS_LITERAL_CSTRING("")); + } else if (aState == hal::SWITCH_STATE_OFF) { + if (headsetConnected) { + UpdateDeviceConnectionState(false, + AUDIO_DEVICE_OUT_WIRED_HEADSET, + NS_LITERAL_CSTRING("")); + } + if (headphoneConnected) { + UpdateDeviceConnectionState(false, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + NS_LITERAL_CSTRING("")); + } + } +} + +void +AudioManager::UpdateDeviceConnectionState(bool aIsConnected, uint32_t aDevice, const nsCString& aDeviceName) +{ +#if ANDROID_VERSION >= 15 + bool isConnected = mConnectedDevices.Get(aDevice, nullptr); + if (isConnected && !aIsConnected) { + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice), + AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, + aDeviceName.get()); + mConnectedDevices.Remove(aDevice); + } else if(!isConnected && aIsConnected) { + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice), + AUDIO_POLICY_DEVICE_STATE_AVAILABLE, + aDeviceName.get()); + mConnectedDevices.Put(aDevice, aDeviceName); + } +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif +#else + NS_NOTREACHED("Doesn't support audio routing on GB version"); +#endif +} + +void +AudioManager::SetAllDeviceConnectionStates() +{ + for (auto iter = mConnectedDevices.Iter(); !iter.Done(); iter.Next()) { + const uint32_t& device = iter.Key(); + nsCString& deviceAddress = iter.Data(); + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(device), + AUDIO_POLICY_DEVICE_STATE_AVAILABLE, + deviceAddress.get()); + } +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(true); +#endif +} + +void +AudioManager::HandleBluetoothStatusChanged(nsISupports* aSubject, + const char* aTopic, + const nsCString aAddress) +{ +#ifdef MOZ_B2G_BT + bool isConnected = false; + if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) { + BluetoothHfpManagerBase* hfp = + static_cast<BluetoothHfpManagerBase*>(aSubject); + isConnected = hfp->IsScoConnected(); + } else { + BluetoothProfileManagerBase* profile = + static_cast<BluetoothProfileManagerBase*>(aSubject); + isConnected = profile->IsConnected(); + } + + if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) { + if (isConnected) { + String8 cmd; + cmd.appendFormat("bt_samplerate=%d", kBtSampleRate); + AudioSystem::setParameters(0, cmd); + SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_BT_SCO); + } else { + int32_t force; + GetForceForUse(nsIAudioManager::USE_COMMUNICATION, &force); + if (force == nsIAudioManager::FORCE_BT_SCO) { + SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_NONE); + } + } + } else if (!strcmp(aTopic, BLUETOOTH_A2DP_STATUS_CHANGED_ID)) { + if (!isConnected && mA2dpSwitchDone) { + RefPtr<AudioManager> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([self, isConnected, aAddress]() { + if (self->mA2dpSwitchDone) { + return; + } + self->UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP, + aAddress); + + String8 cmd("bluetooth_enabled=false"); + AudioSystem::setParameters(0, cmd); + cmd.setTo("A2dpSuspended=true"); + AudioSystem::setParameters(0, cmd); + self->mA2dpSwitchDone = true; + }); + MessageLoop::current()->PostDelayedTask( + MakeAndAddRef<RunnableCallTask>(runnable), 1000); + + mA2dpSwitchDone = false; + } else { + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP, + aAddress); + String8 cmd("bluetooth_enabled=true"); + AudioSystem::setParameters(0, cmd); + cmd.setTo("A2dpSuspended=false"); + AudioSystem::setParameters(0, cmd); + mA2dpSwitchDone = true; +#if ANDROID_VERSION >= 17 + if (AudioSystem::getForceUse(AUDIO_POLICY_FORCE_FOR_MEDIA) == AUDIO_POLICY_FORCE_NO_BT_A2DP) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE); + } +#endif + } + mBluetoothA2dpEnabled = isConnected; + } else if (!strcmp(aTopic, BLUETOOTH_HFP_STATUS_CHANGED_ID)) { + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET, + aAddress); + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, + aAddress); + } else if (!strcmp(aTopic, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID)) { + String8 cmd; + BluetoothHfpManagerBase* hfp = + static_cast<BluetoothHfpManagerBase*>(aSubject); + if (hfp->IsNrecEnabled()) { + cmd.setTo("bt_headset_name=<unknown>;bt_headset_nrec=on"); + AudioSystem::setParameters(0, cmd); + } else { + cmd.setTo("bt_headset_name=<unknown>;bt_headset_nrec=off"); + AudioSystem::setParameters(0, cmd); + } + } +#endif +} + +void +AudioManager::HandleAudioChannelProcessChanged() +{ + // Note: If the user answers a VoIP call (e.g. WebRTC calls) during the + // telephony call (GSM/CDMA calls) the audio manager won't set the + // PHONE_STATE_IN_COMMUNICATION audio state. Once the telephony call finishes + // the RIL plumbing sets the PHONE_STATE_NORMAL audio state. This seems to be + // an issue for the VoIP call but it is not. Once the RIL plumbing sets the + // the PHONE_STATE_NORMAL audio state the AudioManager::mPhoneAudioAgent + // member will call the NotifyStoppedPlaying() method causing that this function will + // be called again and therefore the audio manager sets the + // PHONE_STATE_IN_COMMUNICATION audio state. + + if ((mPhoneState == PHONE_STATE_IN_CALL) || + (mPhoneState == PHONE_STATE_RINGTONE)) { + return; + } + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + bool telephonyChannelIsActive = service && service->TelephonyChannelIsActive(); + telephonyChannelIsActive ? SetPhoneState(PHONE_STATE_IN_COMMUNICATION) : + SetPhoneState(PHONE_STATE_NORMAL); +} + +nsresult +AudioManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if ((strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_HFP_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_A2DP_STATUS_CHANGED_ID) == 0)) { + nsCString address = NS_ConvertUTF16toUTF8(nsDependentString(aData)); + if (address.IsEmpty()) { + NS_WARNING(nsPrintfCString("Invalid address of %s", aTopic).get()); + return NS_ERROR_FAILURE; + } + + HandleBluetoothStatusChanged(aSubject, aTopic, address); + return NS_OK; + } + + else if (!strcmp(aTopic, AUDIO_CHANNEL_PROCESS_CHANGED)) { + HandleAudioChannelProcessChanged(); + return NS_OK; + } + + // To process the volume control on each volume categories according to + // change of settings + else if (!strcmp(aTopic, MOZ_SETTINGS_CHANGE_ID)) { + RootedDictionary<dom::SettingChangeNotification> setting(RootingCx()); + if (!WrappedJSToDictionary(aSubject, setting)) { + return NS_OK; + } + if (!StringBeginsWith(setting.mKey, NS_LITERAL_STRING("audio.volume."))) { + return NS_OK; + } + if (!setting.mValue.isNumber()) { + return NS_OK; + } + + uint32_t volIndex = setting.mValue.toNumber(); + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + if (setting.mKey.EqualsASCII(gVolumeData[idx].mChannelName)) { + SetStreamVolumeIndex(gVolumeData[idx].mStreamType, volIndex); + return NS_OK; + } + } + } + + NS_WARNING("Unexpected topic in AudioManager"); + return NS_ERROR_FAILURE; +} + +static void +NotifyHeadphonesStatus(hal::SwitchState aState) +{ + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + if (aState == hal::SWITCH_STATE_HEADSET) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_HEADSET); + } else if (aState == hal::SWITCH_STATE_HEADPHONE) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_HEADPHONE); + } else if (aState == hal::SWITCH_STATE_OFF) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_OFF); + } else { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_UNKNOWN); + } + } +} + +class HeadphoneSwitchObserver : public hal::SwitchObserver +{ +public: + void Notify(const hal::SwitchEvent& aEvent) { + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + MOZ_ASSERT(audioManager); + audioManager->HandleHeadphoneSwitchEvent(aEvent); + } +}; + +void +AudioManager::HandleHeadphoneSwitchEvent(const hal::SwitchEvent& aEvent) +{ + NotifyHeadphonesStatus(aEvent.status()); + // When user pulled out the headset, a delay of routing here can avoid the leakage of audio from speaker. + if (aEvent.status() == hal::SWITCH_STATE_OFF && mSwitchDone) { + + RefPtr<AudioManager> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([self]() { + if (self->mSwitchDone) { + return; + } + self->UpdateHeadsetConnectionState(hal::SWITCH_STATE_OFF); + self->mSwitchDone = true; + }); + MessageLoop::current()->PostDelayedTask( + MakeAndAddRef<RunnableCallTask>(runnable), 1000); + mSwitchDone = false; + } else if (aEvent.status() != hal::SWITCH_STATE_OFF) { + UpdateHeadsetConnectionState(aEvent.status()); + mSwitchDone = true; + } + // Handle the coexistence of a2dp / headset device, latest one wins. +#if ANDROID_VERSION >= 17 + int32_t forceUse = 0; + GetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, &forceUse); + if (aEvent.status() != hal::SWITCH_STATE_OFF && mBluetoothA2dpEnabled) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NO_BT_A2DP); + } else if (forceUse == AUDIO_POLICY_FORCE_NO_BT_A2DP) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE); + } +#endif +} + +AudioManager::AudioManager() + : mPhoneState(PHONE_STATE_CURRENT) + , mIsVolumeInited(false) + , mAudioOutDevicesUpdated(0) + , mSwitchDone(true) +#if defined(MOZ_B2G_BT) || ANDROID_VERSION >= 17 + , mBluetoothA2dpEnabled(false) +#endif +#ifdef MOZ_B2G_BT + , mA2dpSwitchDone(true) +#endif + , mObserver(new HeadphoneSwitchObserver()) +{ + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx) { + mAudioDeviceTableIdMaps.Put(kAudioDeviceInfos[idx].value, idx); + } + + AudioSystem::setErrorCallback(BinderDeadCallback); +#if ANDROID_VERSION >= 21 + android::sp<GonkAudioPortCallback> callback = new GonkAudioPortCallback(); + AudioSystem::setAudioPortCallback(callback); +#endif + + // Create VolumeStreamStates + for (uint32_t loop = 0; loop < AUDIO_STREAM_CNT; ++loop) { + VolumeStreamState* streamState = + new VolumeStreamState(*this, static_cast<audio_stream_type_t>(loop)); + mStreamStates.AppendElement(streamState); + } + // Initialize stream volumes with default values + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + uint32_t volIndex = sDefaultStreamVolumeTbl[streamType]; + SetStreamVolumeForDevice(streamType, volIndex, AUDIO_DEVICE_OUT_DEFAULT); + } + UpdateCachedActiveDevicesForStreams(); + + RegisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver); + // Initialize headhone/heaset status + UpdateHeadsetConnectionState(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES)); + NotifyHeadphonesStatus(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES)); + + // Get the initial volume index from settings DB during boot up. + InitVolumeFromDatabase(); + + // Gecko only control stream volume not master so set to default value + // directly. + AudioSystem::setMasterVolume(1.0); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth sco status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth a2dp status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_HFP_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth hfp status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth hfp NREC status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, MOZ_SETTINGS_CHANGE_ID, false))) { + NS_WARNING("Failed to add mozsettings-changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, AUDIO_CHANNEL_PROCESS_CHANGED, false))) { + NS_WARNING("Failed to add audio-channel-process-changed observer!"); + } + +} + +AudioManager::~AudioManager() { + AudioSystem::setErrorCallback(nullptr); +#if ANDROID_VERSION >= 21 + AudioSystem::setAudioPortCallback(nullptr); +#endif + hal::UnregisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth sco status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth a2dp status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_HFP_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth hfp status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth hfp NREC status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, MOZ_SETTINGS_CHANGE_ID))) { + NS_WARNING("Failed to remove mozsettings-changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, AUDIO_CHANNEL_PROCESS_CHANGED))) { + NS_WARNING("Failed to remove audio-channel-process-changed!"); + } +} + +static StaticRefPtr<AudioManager> sAudioManager; + +already_AddRefed<AudioManager> +AudioManager::GetInstance() +{ + // Avoid createing AudioManager from content process. + if (!XRE_IsParentProcess()) { + MOZ_CRASH("Non-chrome processes should not get here."); + } + + // Avoid createing multiple AudioManager instance inside main process. + if (!sAudioManager) { + sAudioManager = new AudioManager(); + ClearOnShutdown(&sAudioManager); + } + + RefPtr<AudioManager> audioMgr = sAudioManager.get(); + return audioMgr.forget(); +} + +NS_IMETHODIMP +AudioManager::GetMicrophoneMuted(bool* aMicrophoneMuted) +{ + + if (AudioSystem::isMicrophoneMuted(aMicrophoneMuted)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetMicrophoneMuted(bool aMicrophoneMuted) +{ + if (!AudioSystem::muteMicrophone(aMicrophoneMuted)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AudioManager::GetPhoneState(int32_t* aState) +{ + *aState = mPhoneState; + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetPhoneState(int32_t aState) +{ + if (mPhoneState == aState) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + nsString state; + state.AppendInt(aState); + obs->NotifyObservers(nullptr, "phone-state-changed", state.get()); + } + +#if ANDROID_VERSION < 17 + if (AudioSystem::setPhoneState(aState)) { +#else + if (AudioSystem::setPhoneState(static_cast<audio_mode_t>(aState))) { +#endif + return NS_ERROR_FAILURE; + } + +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif + mPhoneState = aState; + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetForceForUse(int32_t aUsage, int32_t aForce) +{ +#if ANDROID_VERSION >= 15 + status_t status = AudioSystem::setForceUse( + (audio_policy_force_use_t)aUsage, + (audio_policy_forced_cfg_t)aForce); +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif + return status ? NS_ERROR_FAILURE : NS_OK; +#else + NS_NOTREACHED("Doesn't support force routing on GB version"); + return NS_ERROR_UNEXPECTED; +#endif +} + +NS_IMETHODIMP +AudioManager::GetForceForUse(int32_t aUsage, int32_t* aForce) { +#if ANDROID_VERSION >= 15 + *aForce = AudioSystem::getForceUse((audio_policy_force_use_t)aUsage); + return NS_OK; +#else + NS_NOTREACHED("Doesn't support force routing on GB version"); + return NS_ERROR_UNEXPECTED; +#endif +} + +NS_IMETHODIMP +AudioManager::SetAudioChannelVolume(uint32_t aChannel, uint32_t aIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + return SetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex); +} + +NS_IMETHODIMP +AudioManager::GetAudioChannelVolume(uint32_t aChannel, uint32_t* aIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + if (!aIndex) { + return NS_ERROR_NULL_POINTER; + } + + return GetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex); +} + +NS_IMETHODIMP +AudioManager::GetMaxAudioChannelVolume(uint32_t aChannel, uint32_t* aMaxIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + if (!aMaxIndex) { + return NS_ERROR_NULL_POINTER; + } + + *aMaxIndex = mStreamStates[sChannelStreamTbl[aChannel]]->GetMaxIndex(); + return NS_OK; +} + +nsresult +AudioManager::ValidateVolumeIndex(int32_t aStream, uint32_t aIndex) const +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + uint32_t maxIndex = mStreamStates[aStream]->GetMaxIndex(); + if (aIndex > maxIndex) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult +AudioManager::SetStreamVolumeForDevice(int32_t aStream, + uint32_t aIndex, + uint32_t aDevice) +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + int32_t streamAlias = sStreamVolumeAliasTbl[aStream]; + VolumeStreamState* streamState = mStreamStates[streamAlias].get(); + return streamState->SetVolumeIndexToAliasStreams(aIndex, aDevice); +} + +nsresult +AudioManager::SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex) +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + int32_t streamAlias = sStreamVolumeAliasTbl[aStream]; + + nsresult rv; + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + if (streamAlias == sStreamVolumeAliasTbl[streamType]) { + rv = mStreamStates[streamType]->SetVolumeIndexToActiveDevices(aIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + // AUDIO_STREAM_FM is not used on recent gonk. + // AUDIO_STREAM_MUSIC is used for FM radio volume control. +#if ANDROID_VERSION < 19 + if (streamAlias == AUDIO_STREAM_MUSIC && IsFmOutConnected()) { + rv = mStreamStates[AUDIO_STREAM_FM]-> + SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_FM); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } +#endif + + MaybeUpdateVolumeSettingToDatabase(); + return NS_OK; +} + +nsresult +AudioManager::GetStreamVolumeIndex(int32_t aStream, uint32_t *aIndex) +{ + if (!aIndex) { + return NS_ERROR_INVALID_ARG; + } + + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + *aIndex = mStreamStates[aStream]->GetVolumeIndex(); + return NS_OK; +} + +nsAutoCString +AudioManager::AppendDeviceToVolumeSetting(const char* aName, uint32_t aDevice) +{ + nsAutoCString topic; + topic.Assign(aName); + topic.Append("."); + uint32_t index = 0; + DebugOnly<bool> exist = mAudioDeviceTableIdMaps.Get(aDevice, &index); + MOZ_ASSERT(exist); + topic.Append(kAudioDeviceInfos[index].tag); + return topic; +} + +void +AudioManager::InitVolumeFromDatabase() +{ + nsresult rv; + nsCOMPtr<nsISettingsService> service = do_GetService(SETTINGS_SERVICE, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsISettingsServiceLock> lock; + rv = service->CreateLock(nullptr, getter_AddRefs(lock)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<VolumeInitCallback> callback = new VolumeInitCallback(); + MOZ_ASSERT(callback); + callback->GetPromise()->Then(AbstractThread::MainThread(), __func__, this, + &AudioManager::InitDeviceVolumeSucceeded, + &AudioManager::InitDeviceVolumeFailed); + + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + for (uint32_t idx2 = 0; idx2 < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx2) { + lock->Get(AppendDeviceToVolumeSetting(gVolumeData[idx].mChannelName, + kAudioDeviceInfos[idx2].value).get(), + callback); + } + } +} + +void +AudioManager::InitDeviceVolumeSucceeded() +{ + mIsVolumeInited = true; + MaybeUpdateVolumeSettingToDatabase(true); +} + +void +AudioManager::InitDeviceVolumeFailed(const char* aError) +{ + // Default volume of AUDIO_DEVICE_OUT_DEFAULT is already set. + mIsVolumeInited = true; + MaybeUpdateVolumeSettingToDatabase(true); + NS_WARNING(aError); +} + +void +AudioManager::MaybeUpdateVolumeSettingToDatabase(bool aForce) +{ + if (!mIsVolumeInited) { + return; + } + + nsCOMPtr<nsISettingsServiceLock> lock = GetSettingServiceLock(); + if (NS_WARN_IF(!lock)) { + return; + } + + // Send events to update the Gaia volumes + JS::Rooted<JS::Value> value(RootingCx()); + uint32_t volume = 0; + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + VolumeStreamState* streamState = mStreamStates[streamType].get(); + if(!aForce && !streamState->IsDevicesChanged()) { + continue; + } + // Get volume index of active device. + volume = streamState->GetVolumeIndex(); + value.setInt32(volume); + lock->Set(gVolumeData[idx].mChannelName, value, nullptr, nullptr); + } + + // For reducing the code dependency, Gaia doesn't need to know the + // device volume, it only need to care about different volume categories. + // However, we need to send the setting volume to the permanent database, + // so that we can store the volume setting even if the phone reboots. + + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + VolumeStreamState* streamState = mStreamStates[streamType].get(); + + if(!streamState->IsVolumeIndexesChanged()) { + continue; + } + + uint32_t remainingDevices = mAudioOutDevicesUpdated; + for (uint32_t i = 0; remainingDevices != 0; i++) { + uint32_t device = (1 << i); + if ((device & remainingDevices) == 0) { + continue; + } + remainingDevices &= ~device; + if (!mAudioDeviceTableIdMaps.Get(device, nullptr)) { + continue; + } + volume = streamState->GetVolumeIndex(device); + value.setInt32(volume); + lock->Set(AppendDeviceToVolumeSetting(gVolumeData[idx].mChannelName, + device).get(), + value, nullptr, nullptr); + } + } + + // Clear changed flags + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + mStreamStates[streamType]->ClearDevicesChanged(); + mStreamStates[streamType]->ClearVolumeIndexesChanged(); + } + // Clear mAudioOutDevicesUpdated + mAudioOutDevicesUpdated = 0; +} + +void +AudioManager::UpdateCachedActiveDevicesForStreams() +{ + // This function updates cached active devices for streams. + // It is used for optimization of GetDevicesForStream() since L. + // AudioManager could know when active devices + // are changed in AudioPolicyManager by onAudioPortListUpdate(). + // Except it, AudioManager normally do not need to ask AuidoPolicyManager + // about current active devices of streams and could use cached values. + // Before L, onAudioPortListUpdate() does not exist and GetDevicesForStream() + // does not use the cache. Therefore this function do nothing. +#if ANDROID_VERSION >= 21 + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + // Update cached active devices of stream + mStreamStates[streamType]->IsDevicesChanged(false /* aFromCache */); + } +#endif +} + +uint32_t +AudioManager::GetDevicesForStream(int32_t aStream, bool aFromCache) +{ +#if ANDROID_VERSION >= 21 + // Since Lollipop, devices update could be notified by AudioPortCallback. + // Cached values can be used if there is no update. + if (aFromCache) { + return mStreamStates[aStream]->GetLastDevices(); + } +#endif + +#if ANDROID_VERSION >= 17 + audio_devices_t devices = + AudioSystem::getDevicesForStream(static_cast<audio_stream_type_t>(aStream)); + + return static_cast<uint32_t>(devices); +#else + // Per audio out device volume is not supported. + // Use AUDIO_DEVICE_OUT_SPEAKER just to store audio volume to DB. + return AUDIO_DEVICE_OUT_SPEAKER; +#endif +} + +uint32_t +AudioManager::GetDeviceForStream(int32_t aStream) +{ + uint32_t devices = + GetDevicesForStream(static_cast<audio_stream_type_t>(aStream)); + uint32_t device = SelectDeviceFromDevices(devices); + return device; +} + +/* static */ uint32_t +AudioManager::SelectDeviceFromDevices(uint32_t aOutDevices) +{ + uint32_t device = aOutDevices; + + // See android AudioService.getDeviceForStream(). + // AudioPolicyManager expects it. + // See also android AudioPolicyManager::getDeviceForVolume(). + if ((device & (device - 1)) != 0) { + // Multiple device selection. + if ((device & AUDIO_DEVICE_OUT_SPEAKER) != 0) { + device = AUDIO_DEVICE_OUT_SPEAKER; +#if ANDROID_VERSION >= 21 + } else if ((device & AUDIO_DEVICE_OUT_HDMI_ARC) != 0) { + device = AUDIO_DEVICE_OUT_HDMI_ARC; + } else if ((device & AUDIO_DEVICE_OUT_SPDIF) != 0) { + device = AUDIO_DEVICE_OUT_SPDIF; + } else if ((device & AUDIO_DEVICE_OUT_AUX_LINE) != 0) { + device = AUDIO_DEVICE_OUT_AUX_LINE; +#endif + } else { + device &= AUDIO_DEVICE_OUT_ALL_A2DP; + } + } + MOZ_ASSERT(audio_is_output_device(device)); + return device; +} +AudioManager::VolumeStreamState::VolumeStreamState(AudioManager& aManager, + int32_t aStreamType) + : mManager(aManager) + , mStreamType(aStreamType) + , mLastDevices(0) + , mIsDevicesChanged(true) + , mIsVolumeIndexesChanged(true) +{ + InitStreamVolume(); +} + +bool +AudioManager::VolumeStreamState::IsDevicesChanged(bool aFromCache) +{ + uint32_t devices = mManager.GetDevicesForStream(mStreamType, aFromCache); + if (devices != mLastDevices) { + mLastDevices = devices; + mIsDevicesChanged = true; + } + return mIsDevicesChanged; +} + +void +AudioManager::VolumeStreamState::ClearDevicesChanged() +{ + mIsDevicesChanged = false; +} + +bool +AudioManager::VolumeStreamState::IsVolumeIndexesChanged() +{ + return mIsVolumeIndexesChanged; +} + +void +AudioManager::VolumeStreamState::ClearVolumeIndexesChanged() +{ + mIsVolumeIndexesChanged = false; +} + +void +AudioManager::VolumeStreamState::InitStreamVolume() +{ + AudioSystem::initStreamVolume(static_cast<audio_stream_type_t>(mStreamType), + 0, + GetMaxIndex()); +} + +uint32_t +AudioManager::VolumeStreamState::GetMaxIndex() +{ + return sMaxStreamVolumeTbl[mStreamType]; +} + +uint32_t +AudioManager::VolumeStreamState::GetDefaultIndex() +{ + return sDefaultStreamVolumeTbl[mStreamType]; +} + +uint32_t +AudioManager::VolumeStreamState::GetVolumeIndex() +{ + uint32_t device = mManager.GetDeviceForStream(mStreamType); + return GetVolumeIndex(device); +} + +uint32_t +AudioManager::VolumeStreamState::GetVolumeIndex(uint32_t aDevice) +{ + uint32_t index = 0; + bool ret = mVolumeIndexes.Get(aDevice, &index); + if (!ret) { + index = mVolumeIndexes.Get(AUDIO_DEVICE_OUT_DEFAULT); + } + return index; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToActiveDevices(uint32_t aIndex) +{ + uint32_t device = mManager.GetDeviceForStream(mStreamType); + + // Update volume index for device + uint32_t oldVolumeIndex = 0; + bool exist = mVolumeIndexes.Get(device, &oldVolumeIndex); + if (exist && aIndex == oldVolumeIndex) { + // No update + return NS_OK; + } + + // AudioPolicyManager::setStreamVolumeIndex() set volumes of all active + // devices for stream. + nsresult rv; + rv = SetVolumeIndexToConsistentDeviceIfNeeded(aIndex, device); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToAliasStreams(uint32_t aIndex, + uint32_t aDevice) +{ + uint32_t oldVolumeIndex = 0; + bool exist = mVolumeIndexes.Get(aDevice, &oldVolumeIndex); + if (exist && aIndex == oldVolumeIndex) { + // No update + return NS_OK; + } + + nsresult rv = SetVolumeIndexToConsistentDeviceIfNeeded(aIndex, aDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + if ((streamType != mStreamType) && + sStreamVolumeAliasTbl[streamType] == mStreamType) { + // Rescaling of index is not necessary. + rv = mManager.mStreamStates[streamType]-> + SetVolumeIndexToAliasStreams(aIndex, aDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + return NS_OK; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToConsistentDeviceIfNeeded(uint32_t aIndex, uint32_t aDevice) +{ + nsresult rv; + if (aDevice == AUDIO_DEVICE_OUT_SPEAKER || aDevice == AUDIO_DEVICE_OUT_EARPIECE) { + // Set AUDIO_DEVICE_OUT_SPEAKER and AUDIO_DEVICE_OUT_EARPIECE to same volume. + rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_SPEAKER); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_EARPIECE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // No alias device + rv = SetVolumeIndex(aIndex, aDevice); + } + return rv; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndex(uint32_t aIndex, + uint32_t aDevice, + bool aUpdateCache) +{ + status_t rv; +#if ANDROID_VERSION >= 17 + if (aUpdateCache) { + mVolumeIndexes.Put(aDevice, aIndex); + mIsVolumeIndexesChanged = true; + mManager.AudioOutDeviceUpdated(aDevice); + } + + rv = AudioSystem::setStreamVolumeIndex( + static_cast<audio_stream_type_t>(mStreamType), + aIndex, + aDevice); + return rv ? NS_ERROR_FAILURE : NS_OK; +#else + if (aUpdateCache) { + // Per audio out device volume is not supported. + // Use AUDIO_DEVICE_OUT_SPEAKER just to store audio volume to DB. + mVolumeIndexes.Put(AUDIO_DEVICE_OUT_SPEAKER, aIndex); + mIsVolumeIndexesChanged = true; + mManager.AudioOutDeviceUpdated(AUDIO_DEVICE_OUT_SPEAKER); + } + rv = AudioSystem::setStreamVolumeIndex( + static_cast<audio_stream_type_t>(mStreamType), + aIndex); + return rv ? NS_ERROR_FAILURE : NS_OK; +#endif +} + +void +AudioManager::VolumeStreamState::RestoreVolumeIndexToAllDevices() +{ + for (auto iter = mVolumeIndexes.Iter(); !iter.Done(); iter.Next()) { + const uint32_t& key = iter.Key(); + uint32_t& index = iter.Data(); + SetVolumeIndex(key, index, /* aUpdateCache */ false); + } +} + +} /* namespace gonk */ +} /* namespace dom */ +} /* namespace mozilla */ |