diff -r 000000000000 -r 6474c204b198 dom/audiochannel/AudioChannelService.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/audiochannel/AudioChannelService.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,950 @@ +/* -*- 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 "AudioChannelService.h" +#include "AudioChannelServiceChild.h" + +#include "base/basictypes.h" + +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/unused.h" + +#include "mozilla/dom/ContentParent.h" + +#include "nsThreadUtils.h" +#include "nsHashPropertyBag.h" +#include "nsComponentManagerUtils.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" + +#ifdef MOZ_WIDGET_GONK +#include "nsJSUtils.h" +#include "nsCxPusher.h" +#include "nsIAudioManager.h" +#include "SpeakerManagerService.h" +#define NS_AUDIOMANAGER_CONTRACTID "@mozilla.org/telephony/audiomanager;1" +#endif + +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::hal; + +StaticRefPtr gAudioChannelService; + +// Mappings from 'mozaudiochannel' attribute strings to an enumeration. +static const nsAttrValue::EnumTable kMozAudioChannelAttributeTable[] = { + { "normal", (int16_t)AudioChannel::Normal }, + { "content", (int16_t)AudioChannel::Content }, + { "notification", (int16_t)AudioChannel::Notification }, + { "alarm", (int16_t)AudioChannel::Alarm }, + { "telephony", (int16_t)AudioChannel::Telephony }, + { "ringer", (int16_t)AudioChannel::Ringer }, + { "publicnotification", (int16_t)AudioChannel::Publicnotification }, + { nullptr } +}; + +// static +AudioChannelService* +AudioChannelService::GetAudioChannelService() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (XRE_GetProcessType() != GeckoProcessType_Default) { + return AudioChannelServiceChild::GetAudioChannelService(); + } + + // If we already exist, exit early + if (gAudioChannelService) { + return gAudioChannelService; + } + + // Create new instance, register, return + nsRefPtr service = new AudioChannelService(); + NS_ENSURE_TRUE(service, nullptr); + + gAudioChannelService = service; + return gAudioChannelService; +} + +void +AudioChannelService::Shutdown() +{ + if (XRE_GetProcessType() != GeckoProcessType_Default) { + return AudioChannelServiceChild::Shutdown(); + } + + if (gAudioChannelService) { + gAudioChannelService = nullptr; + } +} + +NS_IMPL_ISUPPORTS(AudioChannelService, nsIObserver, nsITimerCallback) + +AudioChannelService::AudioChannelService() +: mCurrentHigherChannel(-1) +, mCurrentVisibleHigherChannel(-1) +, mPlayableHiddenContentChildID(CONTENT_PROCESS_ID_UNKNOWN) +, mDisabled(false) +, mDefChannelChildID(CONTENT_PROCESS_ID_UNKNOWN) +{ + if (XRE_GetProcessType() == GeckoProcessType_Default) { + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "ipc:content-shutdown", false); + obs->AddObserver(this, "xpcom-shutdown", false); +#ifdef MOZ_WIDGET_GONK + // To monitor the volume settings based on audio channel. + obs->AddObserver(this, "mozsettings-changed", false); +#endif + } + } +} + +AudioChannelService::~AudioChannelService() +{ +} + +void +AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent, + AudioChannel aChannel, + bool aWithVideo) +{ + if (mDisabled) { + return; + } + + AudioChannelAgentData* data = new AudioChannelAgentData(aChannel, + true /* aElementHidden */, + AUDIO_CHANNEL_STATE_MUTED /* aState */, + aWithVideo); + mAgents.Put(aAgent, data); + RegisterType(aChannel, CONTENT_PROCESS_ID_MAIN, aWithVideo); + + // If this is the first agent for this window, we must notify the observers. + uint32_t count = CountWindow(aAgent->Window()); + if (count == 1) { + nsCOMPtr observerService = + services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers(ToSupports(aAgent->Window()), + "media-playback", + NS_LITERAL_STRING("active").get()); + } + } +} + +void +AudioChannelService::RegisterType(AudioChannel aChannel, uint64_t aChildID, + bool aWithVideo) +{ + if (mDisabled) { + return; + } + + AudioChannelInternalType type = GetInternalType(aChannel, true); + mChannelCounters[type].AppendElement(aChildID); + + if (XRE_GetProcessType() == GeckoProcessType_Default) { + // Since there is another telephony registered, we can unregister old one + // immediately. + if (mDeferTelChannelTimer && aChannel == AudioChannel::Telephony) { + mDeferTelChannelTimer->Cancel(); + mDeferTelChannelTimer = nullptr; + UnregisterTypeInternal(aChannel, mTimerElementHidden, mTimerChildID, + false); + } + + if (aWithVideo) { + mWithVideoChildIDs.AppendElement(aChildID); + } + + // No hidden content channel can be playable if there is a content channel + // in foreground (bug 855208), nor if there is a normal channel with video + // in foreground (bug 894249). + if (type == AUDIO_CHANNEL_INT_CONTENT || + (type == AUDIO_CHANNEL_INT_NORMAL && + mWithVideoChildIDs.Contains(aChildID))) { + mPlayableHiddenContentChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + // One hidden content channel can be playable only when there is no any + // content channel in the foreground, and no normal channel with video in + // foreground. + else if (type == AUDIO_CHANNEL_INT_CONTENT_HIDDEN && + mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty()) { + mPlayableHiddenContentChildID = aChildID; + } + + // In order to avoid race conditions, it's safer to notify any existing + // agent any time a new one is registered. + SendAudioChannelChangedNotification(aChildID); + Notify(); + } +} + +void +AudioChannelService::UnregisterAudioChannelAgent(AudioChannelAgent* aAgent) +{ + if (mDisabled) { + return; + } + + nsAutoPtr data; + mAgents.RemoveAndForget(aAgent, data); + + if (data) { + UnregisterType(data->mChannel, data->mElementHidden, + CONTENT_PROCESS_ID_MAIN, data->mWithVideo); + } +#ifdef MOZ_WIDGET_GONK + bool active = AnyAudioChannelIsActive(); + for (uint32_t i = 0; i < mSpeakerManager.Length(); i++) { + mSpeakerManager[i]->SetAudioChannelActive(active); + } +#endif + + // If this is the last agent for this window, we must notify the observers. + uint32_t count = CountWindow(aAgent->Window()); + if (count == 0) { + nsCOMPtr observerService = + services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers(ToSupports(aAgent->Window()), + "media-playback", + NS_LITERAL_STRING("inactive").get()); + } + } +} + +void +AudioChannelService::UnregisterType(AudioChannel aChannel, + bool aElementHidden, + uint64_t aChildID, + bool aWithVideo) +{ + if (mDisabled) { + return; + } + + // There are two reasons to defer the decrease of telephony channel. + // 1. User can have time to remove device from his ear before music resuming. + // 2. Give BT SCO to be disconnected before starting to connect A2DP. + if (XRE_GetProcessType() == GeckoProcessType_Default && + aChannel == AudioChannel::Telephony && + (mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY_HIDDEN].Length() + + mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY].Length()) == 1) { + mTimerElementHidden = aElementHidden; + mTimerChildID = aChildID; + mDeferTelChannelTimer = do_CreateInstance("@mozilla.org/timer;1"); + mDeferTelChannelTimer->InitWithCallback(this, 1500, nsITimer::TYPE_ONE_SHOT); + return; + } + + UnregisterTypeInternal(aChannel, aElementHidden, aChildID, aWithVideo); +} + +void +AudioChannelService::UnregisterTypeInternal(AudioChannel aChannel, + bool aElementHidden, + uint64_t aChildID, + bool aWithVideo) +{ + // The array may contain multiple occurrence of this appId but + // this should remove only the first one. + AudioChannelInternalType type = GetInternalType(aChannel, aElementHidden); + MOZ_ASSERT(mChannelCounters[type].Contains(aChildID)); + mChannelCounters[type].RemoveElement(aChildID); + + // In order to avoid race conditions, it's safer to notify any existing + // agent any time a new one is registered. + if (XRE_GetProcessType() == GeckoProcessType_Default) { + // No hidden content channel is playable if the original playable hidden + // process does not need to play audio from background anymore. + if (aChannel == AudioChannel::Content && + mPlayableHiddenContentChildID == aChildID && + !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].Contains(aChildID)) { + mPlayableHiddenContentChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + + if (aWithVideo) { + MOZ_ASSERT(mWithVideoChildIDs.Contains(aChildID)); + mWithVideoChildIDs.RemoveElement(aChildID); + } + + SendAudioChannelChangedNotification(aChildID); + Notify(); + } +} + +void +AudioChannelService::UpdateChannelType(AudioChannel aChannel, + uint64_t aChildID, + bool aElementHidden, + bool aElementWasHidden) +{ + // Calculate the new and old internal type and update the hashtable if needed. + AudioChannelInternalType newType = GetInternalType(aChannel, aElementHidden); + AudioChannelInternalType oldType = GetInternalType(aChannel, aElementWasHidden); + + if (newType != oldType) { + mChannelCounters[newType].AppendElement(aChildID); + MOZ_ASSERT(mChannelCounters[oldType].Contains(aChildID)); + mChannelCounters[oldType].RemoveElement(aChildID); + } + + // No hidden content channel can be playable if there is a content channel + // in foreground (bug 855208), nor if there is a normal channel with video + // in foreground (bug 894249). + if (newType == AUDIO_CHANNEL_INT_CONTENT || + (newType == AUDIO_CHANNEL_INT_NORMAL && + mWithVideoChildIDs.Contains(aChildID))) { + mPlayableHiddenContentChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + // If there is no content channel in foreground and no normal channel with + // video in foreground, the last content channel which goes from foreground + // to background can be playable. + else if (oldType == AUDIO_CHANNEL_INT_CONTENT && + newType == AUDIO_CHANNEL_INT_CONTENT_HIDDEN && + mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty()) { + mPlayableHiddenContentChildID = aChildID; + } +} + +AudioChannelState +AudioChannelService::GetState(AudioChannelAgent* aAgent, bool aElementHidden) +{ + AudioChannelAgentData* data; + if (!mAgents.Get(aAgent, &data)) { + return AUDIO_CHANNEL_STATE_MUTED; + } + + bool oldElementHidden = data->mElementHidden; + // Update visibility. + data->mElementHidden = aElementHidden; + + data->mState = GetStateInternal(data->mChannel, CONTENT_PROCESS_ID_MAIN, + aElementHidden, oldElementHidden); + return data->mState; +} + +AudioChannelState +AudioChannelService::GetStateInternal(AudioChannel aChannel, uint64_t aChildID, + bool aElementHidden, + bool aElementWasHidden) +{ + UpdateChannelType(aChannel, aChildID, aElementHidden, aElementWasHidden); + + // Calculating the new and old type and update the hashtable if needed. + AudioChannelInternalType newType = GetInternalType(aChannel, aElementHidden); + AudioChannelInternalType oldType = GetInternalType(aChannel, + aElementWasHidden); + + if (newType != oldType && + (aChannel == AudioChannel::Content || + (aChannel == AudioChannel::Normal && + mWithVideoChildIDs.Contains(aChildID)))) { + Notify(); + } + + SendAudioChannelChangedNotification(aChildID); + + // Let play any visible audio channel. + if (!aElementHidden) { + if (CheckVolumeFadedCondition(newType, aElementHidden)) { + return AUDIO_CHANNEL_STATE_FADED; + } + return AUDIO_CHANNEL_STATE_NORMAL; + } + + // We are not visible, maybe we have to mute. + if (newType == AUDIO_CHANNEL_INT_NORMAL_HIDDEN || + (newType == AUDIO_CHANNEL_INT_CONTENT_HIDDEN && + // One process can have multiple content channels; and during the + // transition from foreground to background, its content channels will be + // updated with correct visibility status one by one. All its content + // channels should remain playable until all of their visibility statuses + // have been updated as hidden. After all its content channels have been + // updated properly as hidden, mPlayableHiddenContentChildID is used to + // check whether this background process is playable or not. + !(mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].Contains(aChildID) || + (mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty() && + mPlayableHiddenContentChildID == aChildID)))) { + return AUDIO_CHANNEL_STATE_MUTED; + } + + // After checking the condition on normal & content channel, if the state + // is not on muted then checking other higher channels type here. + if (ChannelsActiveWithHigherPriorityThan(newType)) { + MOZ_ASSERT(newType != AUDIO_CHANNEL_INT_NORMAL_HIDDEN); + if (CheckVolumeFadedCondition(newType, aElementHidden)) { + return AUDIO_CHANNEL_STATE_FADED; + } + return AUDIO_CHANNEL_STATE_MUTED; + } + + return AUDIO_CHANNEL_STATE_NORMAL; +} + +bool +AudioChannelService::CheckVolumeFadedCondition(AudioChannelInternalType aType, + bool aElementHidden) +{ + // Only normal & content channels are considered + if (aType > AUDIO_CHANNEL_INT_CONTENT_HIDDEN) { + return false; + } + + // Consider that audio from notification is with short duration + // so just fade the volume not pause it + if (mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION].IsEmpty() && + mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN].IsEmpty()) { + return false; + } + + // Since this element is on the foreground, it can be allowed to play always. + // So return true directly when there is any notification channel alive. + if (aElementHidden == false) { + return true; + } + + // If element is on the background, it is possible paused by channels higher + // then notification. + for (int i = AUDIO_CHANNEL_INT_LAST - 1; + i != AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN; --i) { + if (!mChannelCounters[i].IsEmpty()) { + return false; + } + } + + return true; +} + +bool +AudioChannelService::ContentOrNormalChannelIsActive() +{ + return !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty() || + !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].IsEmpty() || + !mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].IsEmpty(); +} + +bool +AudioChannelService::ProcessContentOrNormalChannelIsActive(uint64_t aChildID) +{ + return mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].Contains(aChildID) || + mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].Contains(aChildID) || + mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].Contains(aChildID); +} + +void +AudioChannelService::SetDefaultVolumeControlChannel(int32_t aChannel, + bool aHidden) +{ + SetDefaultVolumeControlChannelInternal(aChannel, aHidden, + CONTENT_PROCESS_ID_MAIN); +} + +void +AudioChannelService::SetDefaultVolumeControlChannelInternal(int32_t aChannel, + bool aHidden, + uint64_t aChildID) +{ + if (XRE_GetProcessType() != GeckoProcessType_Default) { + return; + } + + // If this child is in the background and mDefChannelChildID is set to + // others then it means other child in the foreground already set it's + // own default channel already. + if (!aHidden && mDefChannelChildID != aChildID) { + return; + } + + mDefChannelChildID = aChildID; + nsString channelName; + + if (aChannel == -1) { + channelName.AssignASCII("unknown"); + } else { + GetAudioChannelString(static_cast(aChannel), channelName); + } + + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "default-volume-channel-changed", + channelName.get()); + } +} + +void +AudioChannelService::SendAudioChannelChangedNotification(uint64_t aChildID) +{ + if (XRE_GetProcessType() != GeckoProcessType_Default) { + return; + } + + nsRefPtr props = new nsHashPropertyBag(); + props->SetPropertyAsUint64(NS_LITERAL_STRING("childID"), aChildID); + + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(static_cast(props), + "audio-channel-process-changed", nullptr); + } + + // Calculating the most important active channel. + int32_t higher = -1; + + // Top-Down in the hierarchy for visible elements + if (!mChannelCounters[AUDIO_CHANNEL_INT_PUBLICNOTIFICATION].IsEmpty()) { + higher = static_cast(AudioChannel::Publicnotification); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_RINGER].IsEmpty()) { + higher = static_cast(AudioChannel::Ringer); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY].IsEmpty()) { + higher = static_cast(AudioChannel::Telephony); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_ALARM].IsEmpty()) { + higher = static_cast(AudioChannel::Alarm); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION].IsEmpty()) { + higher = static_cast(AudioChannel::Notification); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty()) { + higher = static_cast(AudioChannel::Content); + } + + else if (!mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].IsEmpty()) { + higher = static_cast(AudioChannel::Normal); + } + + int32_t visibleHigher = higher; + + // Top-Down in the hierarchy for non-visible elements + // And we can ignore normal channel because it can't play in the background. + int32_t index; + for (index = 0; kMozAudioChannelAttributeTable[index].tag; ++index); + + for (--index; + kMozAudioChannelAttributeTable[index].value > higher && + kMozAudioChannelAttributeTable[index].value > (int16_t)AudioChannel::Normal; + --index) { + if (kMozAudioChannelAttributeTable[index].value == (int16_t)AudioChannel::Content && + mPlayableHiddenContentChildID != CONTENT_PROCESS_ID_UNKNOWN) { + higher = kMozAudioChannelAttributeTable[index].value; + } + + // Each channel type will be split to fg and bg for recording the state, + // so here need to do a translation. + if (!mChannelCounters[index * 2 + 1].IsEmpty()) { + higher = kMozAudioChannelAttributeTable[index].value; + break; + } + } + + if (higher != mCurrentHigherChannel) { + mCurrentHigherChannel = higher; + + nsString channelName; + if (mCurrentHigherChannel != -1) { + GetAudioChannelString(static_cast(mCurrentHigherChannel), + channelName); + } else { + channelName.AssignLiteral("none"); + } + + if (obs) { + obs->NotifyObservers(nullptr, "audio-channel-changed", channelName.get()); + } + } + + if (visibleHigher != mCurrentVisibleHigherChannel) { + mCurrentVisibleHigherChannel = visibleHigher; + + nsString channelName; + if (mCurrentVisibleHigherChannel != -1) { + GetAudioChannelString(static_cast(mCurrentVisibleHigherChannel), + channelName); + } else { + channelName.AssignLiteral("none"); + } + + if (obs) { + obs->NotifyObservers(nullptr, "visible-audio-channel-changed", channelName.get()); + } + } +} + +PLDHashOperator +AudioChannelService::NotifyEnumerator(AudioChannelAgent* aAgent, + AudioChannelAgentData* aData, void* aUnused) +{ + MOZ_ASSERT(aAgent); + aAgent->NotifyAudioChannelStateChanged(); + return PL_DHASH_NEXT; +} + +void +AudioChannelService::Notify() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Notify any agent for the main process. + mAgents.EnumerateRead(NotifyEnumerator, nullptr); + + // Notify for the child processes. + nsTArray children; + ContentParent::GetAll(children); + for (uint32_t i = 0; i < children.Length(); i++) { + unused << children[i]->SendAudioChannelNotify(); + } +} + +NS_IMETHODIMP +AudioChannelService::Notify(nsITimer* aTimer) +{ + UnregisterTypeInternal(AudioChannel::Telephony, mTimerElementHidden, + mTimerChildID, false); + mDeferTelChannelTimer = nullptr; + return NS_OK; +} + +bool +AudioChannelService::AnyAudioChannelIsActive() +{ + for (int i = AUDIO_CHANNEL_INT_LAST - 1; + i >= AUDIO_CHANNEL_INT_NORMAL; --i) { + if (!mChannelCounters[i].IsEmpty()) { + return true; + } + } + + return false; +} + +bool +AudioChannelService::ChannelsActiveWithHigherPriorityThan( + AudioChannelInternalType aType) +{ + for (int i = AUDIO_CHANNEL_INT_LAST - 1; + i != AUDIO_CHANNEL_INT_CONTENT_HIDDEN; --i) { + if (i == aType) { + return false; + } + + if (!mChannelCounters[i].IsEmpty()) { + return true; + } + } + + return false; +} + +NS_IMETHODIMP +AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) +{ + if (!strcmp(aTopic, "xpcom-shutdown")) { + mDisabled = true; + } + + if (!strcmp(aTopic, "ipc:content-shutdown")) { + nsCOMPtr props = do_QueryInterface(aSubject); + if (!props) { + NS_WARNING("ipc:content-shutdown message without property bag as subject"); + return NS_OK; + } + + int32_t index; + uint64_t childID = 0; + nsresult rv = props->GetPropertyAsUint64(NS_LITERAL_STRING("childID"), + &childID); + if (NS_SUCCEEDED(rv)) { + for (int32_t type = AUDIO_CHANNEL_INT_NORMAL; + type < AUDIO_CHANNEL_INT_LAST; + ++type) { + + while ((index = mChannelCounters[type].IndexOf(childID)) != -1) { + mChannelCounters[type].RemoveElementAt(index); + } + } + + // No hidden content channel is playable if the original playable hidden + // process shuts down. + if (mPlayableHiddenContentChildID == childID) { + mPlayableHiddenContentChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + + while ((index = mWithVideoChildIDs.IndexOf(childID)) != -1) { + mWithVideoChildIDs.RemoveElementAt(index); + } + + // We don't have to remove the agents from the mAgents hashtable because if + // that table contains only agents running on the same process. + + SendAudioChannelChangedNotification(childID); + Notify(); + + if (mDefChannelChildID == childID) { + SetDefaultVolumeControlChannelInternal(-1, false, childID); + mDefChannelChildID = CONTENT_PROCESS_ID_UNKNOWN; + } + } else { + NS_WARNING("ipc:content-shutdown message without childID property"); + } + } +#ifdef MOZ_WIDGET_GONK + // To process the volume control on each audio channel according to + // change of settings + else if (!strcmp(aTopic, "mozsettings-changed")) { + AutoSafeJSContext cx; + nsDependentString dataStr(aData); + JS::Rooted val(cx); + if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || + !val.isObject()) { + return NS_OK; + } + + JS::Rooted obj(cx, &val.toObject()); + JS::Rooted key(cx); + if (!JS_GetProperty(cx, obj, "key", &key) || + !key.isString()) { + return NS_OK; + } + + JS::Rooted jsKey(cx, JS::ToString(cx, key)); + if (!jsKey) { + return NS_OK; + } + nsDependentJSString keyStr; + if (!keyStr.init(cx, jsKey) || keyStr.Find("audio.volume.", 0, false)) { + return NS_OK; + } + + JS::Rooted value(cx); + if (!JS_GetProperty(cx, obj, "value", &value) || !value.isInt32()) { + return NS_OK; + } + + nsCOMPtr audioManager = do_GetService(NS_AUDIOMANAGER_CONTRACTID); + NS_ENSURE_TRUE(audioManager, NS_OK); + + int32_t index = value.toInt32(); + if (keyStr.EqualsLiteral("audio.volume.content")) { + audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Content, index); + } else if (keyStr.EqualsLiteral("audio.volume.notification")) { + audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Notification, index); + } else if (keyStr.EqualsLiteral("audio.volume.alarm")) { + audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Alarm, index); + } else if (keyStr.EqualsLiteral("audio.volume.telephony")) { + audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Telephony, index); + } else if (!keyStr.EqualsLiteral("audio.volume.bt_sco")) { + // bt_sco is not a valid audio channel so we manipulate it in + // AudioManager.cpp. And the others should not be used. + // We didn't use MOZ_ASSUME_UNREACHABLE here because any web content who + // has permission of mozSettings can set any names then it can be easy to + // crash the B2G. + NS_WARNING("unexpected audio channel for volume control"); + } + } +#endif + + return NS_OK; +} + +AudioChannelService::AudioChannelInternalType +AudioChannelService::GetInternalType(AudioChannel aChannel, + bool aElementHidden) +{ + switch (aChannel) { + case AudioChannel::Normal: + return aElementHidden + ? AUDIO_CHANNEL_INT_NORMAL_HIDDEN + : AUDIO_CHANNEL_INT_NORMAL; + + case AudioChannel::Content: + return aElementHidden + ? AUDIO_CHANNEL_INT_CONTENT_HIDDEN + : AUDIO_CHANNEL_INT_CONTENT; + + case AudioChannel::Notification: + return aElementHidden + ? AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN + : AUDIO_CHANNEL_INT_NOTIFICATION; + + case AudioChannel::Alarm: + return aElementHidden + ? AUDIO_CHANNEL_INT_ALARM_HIDDEN + : AUDIO_CHANNEL_INT_ALARM; + + case AudioChannel::Telephony: + return aElementHidden + ? AUDIO_CHANNEL_INT_TELEPHONY_HIDDEN + : AUDIO_CHANNEL_INT_TELEPHONY; + + case AudioChannel::Ringer: + return aElementHidden + ? AUDIO_CHANNEL_INT_RINGER_HIDDEN + : AUDIO_CHANNEL_INT_RINGER; + + case AudioChannel::Publicnotification: + return aElementHidden + ? AUDIO_CHANNEL_INT_PUBLICNOTIFICATION_HIDDEN + : AUDIO_CHANNEL_INT_PUBLICNOTIFICATION; + + default: + break; + } + + MOZ_CRASH("unexpected audio channel"); +} + +struct RefreshAgentsVolumeData +{ + RefreshAgentsVolumeData(nsPIDOMWindow* aWindow) + : mWindow(aWindow) + {} + + nsPIDOMWindow* mWindow; + nsTArray> mAgents; +}; + +PLDHashOperator +AudioChannelService::RefreshAgentsVolumeEnumerator(AudioChannelAgent* aAgent, + AudioChannelAgentData* aUnused, + void* aPtr) +{ + MOZ_ASSERT(aAgent); + RefreshAgentsVolumeData* data = static_cast(aPtr); + MOZ_ASSERT(data); + + nsCOMPtr window = do_QueryInterface(aAgent->Window()); + if (window && !window->IsInnerWindow()) { + window = window->GetCurrentInnerWindow(); + } + + if (window == data->mWindow) { + data->mAgents.AppendElement(aAgent); + } + + return PL_DHASH_NEXT; +} +void +AudioChannelService::RefreshAgentsVolume(nsPIDOMWindow* aWindow) +{ + RefreshAgentsVolumeData data(aWindow); + mAgents.EnumerateRead(RefreshAgentsVolumeEnumerator, &data); + + for (uint32_t i = 0; i < data.mAgents.Length(); ++i) { + data.mAgents[i]->WindowVolumeChanged(); + } +} + +struct CountWindowData +{ + CountWindowData(nsIDOMWindow* aWindow) + : mWindow(aWindow) + , mCount(0) + {} + + nsIDOMWindow* mWindow; + uint32_t mCount; +}; + +PLDHashOperator +AudioChannelService::CountWindowEnumerator(AudioChannelAgent* aAgent, + AudioChannelAgentData* aUnused, + void* aPtr) +{ + CountWindowData* data = static_cast(aPtr); + MOZ_ASSERT(aAgent); + + if (aAgent->Window() == data->mWindow) { + ++data->mCount; + } + + return PL_DHASH_NEXT; +} + +uint32_t +AudioChannelService::CountWindow(nsIDOMWindow* aWindow) +{ + CountWindowData data(aWindow); + mAgents.EnumerateRead(CountWindowEnumerator, &data); + return data.mCount; +} + +/* static */ const nsAttrValue::EnumTable* +AudioChannelService::GetAudioChannelTable() +{ + return kMozAudioChannelAttributeTable; +} + +/* static */ AudioChannel +AudioChannelService::GetAudioChannel(const nsAString& aChannel) +{ + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (aChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + return static_cast(kMozAudioChannelAttributeTable[i].value); + } + } + + return AudioChannel::Normal; +} + +/* static */ AudioChannel +AudioChannelService::GetDefaultAudioChannel() +{ + nsString audioChannel = Preferences::GetString("media.defaultAudioChannel"); + if (audioChannel.IsEmpty()) { + return AudioChannel::Normal; + } + + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + return static_cast(kMozAudioChannelAttributeTable[i].value); + } + } + + return AudioChannel::Normal; +} + +/* static */ void +AudioChannelService::GetAudioChannelString(AudioChannel aChannel, + nsAString& aString) +{ + aString.AssignASCII("normal"); + + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (aChannel == + static_cast(kMozAudioChannelAttributeTable[i].value)) { + aString.AssignASCII(kMozAudioChannelAttributeTable[i].tag); + break; + } + } +} + +/* static */ void +AudioChannelService::GetDefaultAudioChannelString(nsAString& aString) +{ + aString.AssignASCII("normal"); + + nsString audioChannel = Preferences::GetString("media.defaultAudioChannel"); + if (!audioChannel.IsEmpty()) { + for (uint32_t i = 0; kMozAudioChannelAttributeTable[i].tag; ++i) { + if (audioChannel.EqualsASCII(kMozAudioChannelAttributeTable[i].tag)) { + aString = audioChannel; + break; + } + } + } +}