michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "MediaManager.h" michael@0: #include "MediaPermissionGonk.h" michael@0: michael@0: #include "nsCOMPtr.h" michael@0: #include "nsCxPusher.h" michael@0: #include "nsIContentPermissionPrompt.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIDOMNavigatorUserMedia.h" michael@0: #include "nsIStringEnumerator.h" michael@0: #include "nsISupportsArray.h" michael@0: #include "nsJSUtils.h" michael@0: #include "nsPIDOMWindow.h" michael@0: #include "nsTArray.h" michael@0: #include "GetUserMediaRequest.h" michael@0: #include "PCOMContentPermissionRequestChild.h" michael@0: #include "mozilla/dom/PBrowserChild.h" michael@0: #include "mozilla/dom/TabChild.h" michael@0: #include "mozilla/dom/MediaStreamTrackBinding.h" michael@0: #include "nsISupportsPrimitives.h" michael@0: #include "nsServiceManagerUtils.h" michael@0: #include "nsArrayUtils.h" michael@0: #include "nsContentPermissionHelper.h" michael@0: #include "mozilla/dom/PermissionMessageUtils.h" michael@0: michael@0: #define AUDIO_PERMISSION_NAME "audio-capture" michael@0: #define VIDEO_PERMISSION_NAME "video-capture" michael@0: michael@0: using namespace mozilla::dom; michael@0: michael@0: namespace mozilla { michael@0: michael@0: static MediaPermissionManager *gMediaPermMgr = nullptr; michael@0: michael@0: static uint32_t michael@0: ConvertArrayToPermissionRequest(nsIArray* aSrcArray, michael@0: nsTArray& aDesArray) michael@0: { michael@0: uint32_t len = 0; michael@0: aSrcArray->GetLength(&len); michael@0: for (uint32_t i = 0; i < len; i++) { michael@0: nsCOMPtr cpt = do_QueryElementAt(aSrcArray, i); michael@0: nsAutoCString type; michael@0: nsAutoCString access; michael@0: cpt->GetType(type); michael@0: cpt->GetAccess(access); michael@0: michael@0: nsCOMPtr optionArray; michael@0: cpt->GetOptions(getter_AddRefs(optionArray)); michael@0: uint32_t optionsLength = 0; michael@0: optionArray->GetLength(&optionsLength); michael@0: nsTArray options; michael@0: for (uint32_t j = 0; j < optionsLength; ++j) { michael@0: nsCOMPtr isupportsString = do_QueryElementAt(optionArray, j); michael@0: if (isupportsString) { michael@0: nsString option; michael@0: isupportsString->GetData(option); michael@0: options.AppendElement(option); michael@0: } michael@0: } michael@0: michael@0: aDesArray.AppendElement(PermissionRequest(type, access, options)); michael@0: } michael@0: return len; michael@0: } michael@0: michael@0: static void michael@0: CreateDeviceNameList(nsTArray > &aDevices, michael@0: nsTArray &aDeviceNameList) michael@0: { michael@0: for (uint32_t i = 0; i < aDevices.Length(); ++i) { michael@0: nsString name; michael@0: nsresult rv = aDevices[i]->GetName(name); michael@0: NS_ENSURE_SUCCESS_VOID(rv); michael@0: aDeviceNameList.AppendElement(name); michael@0: } michael@0: } michael@0: michael@0: static already_AddRefed michael@0: FindDeviceByName(nsTArray > &aDevices, michael@0: const nsAString &aDeviceName) michael@0: { michael@0: for (uint32_t i = 0; i < aDevices.Length(); ++i) { michael@0: nsCOMPtr device = aDevices[i]; michael@0: nsString deviceName; michael@0: device->GetName(deviceName); michael@0: if (deviceName.Equals(aDeviceName)) { michael@0: return device.forget(); michael@0: } michael@0: } michael@0: michael@0: return nullptr; michael@0: } michael@0: michael@0: // Helper function for notifying permission granted michael@0: static nsresult michael@0: NotifyPermissionAllow(const nsAString &aCallID, nsTArray > &aDevices) michael@0: { michael@0: nsresult rv; michael@0: nsCOMPtr array; michael@0: rv = NS_NewISupportsArray(getter_AddRefs(array)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: for (uint32_t i = 0; i < aDevices.Length(); ++i) { michael@0: rv = array->AppendElement(aDevices.ElementAt(i)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); michael@0: michael@0: return obs->NotifyObservers(array, "getUserMedia:response:allow", michael@0: aCallID.BeginReading()); michael@0: } michael@0: michael@0: // Helper function for notifying permision denial or error michael@0: static nsresult michael@0: NotifyPermissionDeny(const nsAString &aCallID, const nsAString &aErrorMsg) michael@0: { michael@0: nsresult rv; michael@0: nsCOMPtr supportsString = michael@0: do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = supportsString->SetData(aErrorMsg); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); michael@0: michael@0: return obs->NotifyObservers(supportsString, "getUserMedia:response:deny", michael@0: aCallID.BeginReading()); michael@0: } michael@0: michael@0: namespace { michael@0: michael@0: /** michael@0: * MediaPermissionRequest will send a prompt ipdl request to b2g process according michael@0: * to its owned type. michael@0: */ michael@0: class MediaPermissionRequest : public nsIContentPermissionRequest michael@0: , public PCOMContentPermissionRequestChild michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSICONTENTPERMISSIONREQUEST michael@0: michael@0: MediaPermissionRequest(nsRefPtr &aRequest, michael@0: nsTArray > &aDevices); michael@0: virtual ~MediaPermissionRequest() {} michael@0: michael@0: // It will be called when prompt dismissed. michael@0: virtual bool Recv__delete__(const bool &allow, michael@0: const InfallibleTArray& choices) MOZ_OVERRIDE; michael@0: virtual void IPDLRelease() MOZ_OVERRIDE { Release(); } michael@0: michael@0: already_AddRefed GetOwner(); michael@0: michael@0: private: michael@0: nsresult DoAllow(const nsString &audioDevice, const nsString &videoDevice); michael@0: michael@0: bool mAudio; // Request for audio permission michael@0: bool mVideo; // Request for video permission michael@0: nsRefPtr mRequest; michael@0: nsTArray > mAudioDevices; // candidate audio devices michael@0: nsTArray > mVideoDevices; // candidate video devices michael@0: }; michael@0: michael@0: // MediaPermissionRequest michael@0: NS_IMPL_ISUPPORTS(MediaPermissionRequest, nsIContentPermissionRequest) michael@0: michael@0: MediaPermissionRequest::MediaPermissionRequest(nsRefPtr &aRequest, michael@0: nsTArray > &aDevices) michael@0: : mRequest(aRequest) michael@0: { michael@0: dom::MediaStreamConstraints constraints; michael@0: mRequest->GetConstraints(constraints); michael@0: michael@0: mAudio = !constraints.mAudio.IsBoolean() || constraints.mAudio.GetAsBoolean(); michael@0: mVideo = !constraints.mVideo.IsBoolean() || constraints.mVideo.GetAsBoolean(); michael@0: michael@0: for (uint32_t i = 0; i < aDevices.Length(); ++i) { michael@0: nsCOMPtr device(aDevices[i]); michael@0: nsAutoString deviceType; michael@0: device->GetType(deviceType); michael@0: if (mAudio && deviceType.EqualsLiteral("audio")) { michael@0: mAudioDevices.AppendElement(device); michael@0: } michael@0: if (mVideo && deviceType.EqualsLiteral("video")) { michael@0: mVideoDevices.AppendElement(device); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // nsIContentPermissionRequest methods michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::GetTypes(nsIArray** aTypes) michael@0: { michael@0: nsCOMPtr types = do_CreateInstance(NS_ARRAY_CONTRACTID); michael@0: //XXX append device list michael@0: if (mAudio) { michael@0: nsTArray audioDeviceNames; michael@0: CreateDeviceNameList(mAudioDevices, audioDeviceNames); michael@0: nsCOMPtr AudioType = michael@0: new ContentPermissionType(NS_LITERAL_CSTRING(AUDIO_PERMISSION_NAME), michael@0: NS_LITERAL_CSTRING("unused"), michael@0: audioDeviceNames); michael@0: types->AppendElement(AudioType, false); michael@0: } michael@0: if (mVideo) { michael@0: nsTArray videoDeviceNames; michael@0: CreateDeviceNameList(mVideoDevices, videoDeviceNames); michael@0: nsCOMPtr VideoType = michael@0: new ContentPermissionType(NS_LITERAL_CSTRING(VIDEO_PERMISSION_NAME), michael@0: NS_LITERAL_CSTRING("unused"), michael@0: videoDeviceNames); michael@0: types->AppendElement(VideoType, false); michael@0: } michael@0: NS_IF_ADDREF(*aTypes = types); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::GetPrincipal(nsIPrincipal **aRequestingPrincipal) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aRequestingPrincipal); michael@0: michael@0: nsCOMPtr window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(mRequest->InnerWindowID())); michael@0: NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); michael@0: michael@0: nsCOMPtr doc = window->GetExtantDoc(); michael@0: NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); michael@0: michael@0: NS_ADDREF(*aRequestingPrincipal = doc->NodePrincipal()); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::GetWindow(nsIDOMWindow** aRequestingWindow) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aRequestingWindow); michael@0: nsCOMPtr window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(mRequest->InnerWindowID())); michael@0: window.forget(aRequestingWindow); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::GetElement(nsIDOMElement** aRequestingElement) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aRequestingElement); michael@0: *aRequestingElement = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::Cancel() michael@0: { michael@0: nsString callID; michael@0: mRequest->GetCallID(callID); michael@0: NotifyPermissionDeny(callID, NS_LITERAL_STRING("Permission Denied")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaPermissionRequest::Allow(JS::HandleValue aChoices) michael@0: { michael@0: // check if JS object michael@0: if (!aChoices.isObject()) { michael@0: MOZ_ASSERT(false, "Not a correct format of PermissionChoice"); michael@0: return NS_ERROR_INVALID_ARG; michael@0: } michael@0: // iterate through audio-capture and video-capture michael@0: AutoSafeJSContext cx; michael@0: JS::Rooted obj(cx, &aChoices.toObject()); michael@0: JSAutoCompartment ac(cx, obj); michael@0: JS::Rooted v(cx); michael@0: michael@0: // get selected audio device name michael@0: nsString audioDevice; michael@0: if (mAudio) { michael@0: if (!JS_GetProperty(cx, obj, AUDIO_PERMISSION_NAME, &v) || !v.isString()) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: nsDependentJSString deviceName; michael@0: if (!deviceName.init(cx, v)) { michael@0: MOZ_ASSERT(false, "Couldn't initialize string from aChoices"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: audioDevice = deviceName; michael@0: } michael@0: michael@0: // get selected video device name michael@0: nsString videoDevice; michael@0: if (mVideo) { michael@0: if (!JS_GetProperty(cx, obj, VIDEO_PERMISSION_NAME, &v) || !v.isString()) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: nsDependentJSString deviceName; michael@0: if (!deviceName.init(cx, v)) { michael@0: MOZ_ASSERT(false, "Couldn't initialize string from aChoices"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: videoDevice = deviceName; michael@0: } michael@0: michael@0: return DoAllow(audioDevice, videoDevice); michael@0: } michael@0: michael@0: nsresult michael@0: MediaPermissionRequest::DoAllow(const nsString &audioDevice, michael@0: const nsString &videoDevice) michael@0: { michael@0: nsTArray > selectedDevices; michael@0: if (mAudio) { michael@0: nsCOMPtr device = michael@0: FindDeviceByName(mAudioDevices, audioDevice); michael@0: if (device) { michael@0: selectedDevices.AppendElement(device); michael@0: } michael@0: } michael@0: michael@0: if (mVideo) { michael@0: nsCOMPtr device = michael@0: FindDeviceByName(mVideoDevices, videoDevice); michael@0: if (device) { michael@0: selectedDevices.AppendElement(device); michael@0: } michael@0: } michael@0: michael@0: nsString callID; michael@0: mRequest->GetCallID(callID); michael@0: return NotifyPermissionAllow(callID, selectedDevices); michael@0: } michael@0: michael@0: already_AddRefed michael@0: MediaPermissionRequest::GetOwner() michael@0: { michael@0: nsCOMPtr window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(mRequest->InnerWindowID())); michael@0: return window.forget(); michael@0: } michael@0: michael@0: //PCOMContentPermissionRequestChild michael@0: bool michael@0: MediaPermissionRequest::Recv__delete__(const bool& allow, michael@0: const InfallibleTArray& choices) michael@0: { michael@0: if (allow) { michael@0: // get selected device name for audio and video michael@0: nsString audioDevice, videoDevice; michael@0: for (uint32_t i = 0; i < choices.Length(); ++i) { michael@0: const nsString &choice = choices[i].choice(); michael@0: if (choices[i].type().EqualsLiteral(AUDIO_PERMISSION_NAME)) { michael@0: audioDevice = choice; michael@0: } else if (choices[i].type().EqualsLiteral(VIDEO_PERMISSION_NAME)) { michael@0: videoDevice = choice; michael@0: } michael@0: } michael@0: (void) DoAllow(audioDevice, videoDevice); michael@0: } else { michael@0: (void) Cancel(); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: // Success callback for MediaManager::GetUserMediaDevices(). michael@0: class MediaDeviceSuccessCallback: public nsIGetUserMediaDevicesSuccessCallback michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSIGETUSERMEDIADEVICESSUCCESSCALLBACK michael@0: michael@0: MediaDeviceSuccessCallback(nsRefPtr &aRequest) michael@0: : mRequest(aRequest) {} michael@0: virtual ~MediaDeviceSuccessCallback() {} michael@0: michael@0: private: michael@0: nsresult DoPrompt(nsRefPtr &req); michael@0: nsRefPtr mRequest; michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(MediaDeviceSuccessCallback, nsIGetUserMediaDevicesSuccessCallback) michael@0: michael@0: // nsIGetUserMediaDevicesSuccessCallback method michael@0: NS_IMETHODIMP michael@0: MediaDeviceSuccessCallback::OnSuccess(nsIVariant* aDevices) michael@0: { michael@0: nsIID elementIID; michael@0: uint16_t elementType; michael@0: void* rawArray; michael@0: uint32_t arrayLen; michael@0: michael@0: nsresult rv; michael@0: rv = aDevices->GetAsArray(&elementType, &elementIID, &arrayLen, &rawArray); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (elementType != nsIDataType::VTYPE_INTERFACE) { michael@0: NS_Free(rawArray); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: // Create array for nsIMediaDevice michael@0: nsTArray > devices; michael@0: michael@0: nsISupports **supportsArray = reinterpret_cast(rawArray); michael@0: for (uint32_t i = 0; i < arrayLen; ++i) { michael@0: nsCOMPtr device(do_QueryInterface(supportsArray[i])); michael@0: devices.AppendElement(device); michael@0: NS_IF_RELEASE(supportsArray[i]); // explicitly decrease reference count for raw pointer michael@0: } michael@0: NS_Free(rawArray); // explicitly free for the memory from nsIVariant::GetAsArray michael@0: michael@0: // Send MediaPermissionRequest michael@0: nsRefPtr req = new MediaPermissionRequest(mRequest, devices); michael@0: rv = DoPrompt(req); michael@0: michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Trigger permission prompt UI michael@0: nsresult michael@0: MediaDeviceSuccessCallback::DoPrompt(nsRefPtr &req) michael@0: { michael@0: // for content process michael@0: if (XRE_GetProcessType() == GeckoProcessType_Content) { michael@0: MOZ_ASSERT(NS_IsMainThread()); // IPC can only be execute on main thread. michael@0: michael@0: nsresult rv; michael@0: michael@0: nsCOMPtr window(req->GetOwner()); michael@0: NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); michael@0: michael@0: dom::TabChild* child = dom::TabChild::GetFrom(window->GetDocShell()); michael@0: NS_ENSURE_TRUE(child, NS_ERROR_FAILURE); michael@0: michael@0: nsCOMPtr typeArray; michael@0: rv = req->GetTypes(getter_AddRefs(typeArray)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsTArray permArray; michael@0: ConvertArrayToPermissionRequest(typeArray, permArray); michael@0: michael@0: nsCOMPtr principal; michael@0: rv = req->GetPrincipal(getter_AddRefs(principal)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: req->AddRef(); michael@0: child->SendPContentPermissionRequestConstructor(req, michael@0: permArray, michael@0: IPC::Principal(principal)); michael@0: michael@0: req->Sendprompt(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // for chrome process michael@0: nsCOMPtr prompt = michael@0: do_GetService(NS_CONTENT_PERMISSION_PROMPT_CONTRACTID); michael@0: if (prompt) { michael@0: prompt->Prompt(req); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Error callback for MediaManager::GetUserMediaDevices() michael@0: class MediaDeviceErrorCallback: public nsIDOMGetUserMediaErrorCallback michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSIDOMGETUSERMEDIAERRORCALLBACK michael@0: michael@0: MediaDeviceErrorCallback(const nsAString &aCallID) michael@0: : mCallID(aCallID) {} michael@0: michael@0: virtual ~MediaDeviceErrorCallback() {} michael@0: michael@0: private: michael@0: const nsString mCallID; michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(MediaDeviceErrorCallback, nsIDOMGetUserMediaErrorCallback) michael@0: michael@0: // nsIDOMGetUserMediaErrorCallback method michael@0: NS_IMETHODIMP michael@0: MediaDeviceErrorCallback::OnError(const nsAString &aError) michael@0: { michael@0: return NotifyPermissionDeny(mCallID, aError); michael@0: } michael@0: michael@0: } // namespace anonymous michael@0: michael@0: // MediaPermissionManager michael@0: NS_IMPL_ISUPPORTS(MediaPermissionManager, nsIObserver) michael@0: michael@0: MediaPermissionManager* michael@0: MediaPermissionManager::GetInstance() michael@0: { michael@0: if (!gMediaPermMgr) { michael@0: gMediaPermMgr = new MediaPermissionManager(); michael@0: } michael@0: michael@0: return gMediaPermMgr; michael@0: } michael@0: michael@0: MediaPermissionManager::MediaPermissionManager() michael@0: { michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: if (obs) { michael@0: obs->AddObserver(this, "getUserMedia:request", false); michael@0: obs->AddObserver(this, "xpcom-shutdown", false); michael@0: } michael@0: } michael@0: michael@0: MediaPermissionManager::~MediaPermissionManager() michael@0: { michael@0: this->Deinit(); michael@0: } michael@0: michael@0: nsresult michael@0: MediaPermissionManager::Deinit() michael@0: { michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: if (obs) { michael@0: obs->RemoveObserver(this, "getUserMedia:request"); michael@0: obs->RemoveObserver(this, "xpcom-shutdown"); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // nsIObserver method michael@0: NS_IMETHODIMP michael@0: MediaPermissionManager::Observe(nsISupports* aSubject, const char* aTopic, michael@0: const char16_t* aData) michael@0: { michael@0: nsresult rv; michael@0: if (!strcmp(aTopic, "getUserMedia:request")) { michael@0: nsRefPtr req = michael@0: static_cast(aSubject); michael@0: rv = HandleRequest(req); michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: nsString callID; michael@0: req->GetCallID(callID); michael@0: NotifyPermissionDeny(callID, NS_LITERAL_STRING("unable to enumerate media device")); michael@0: } michael@0: } else if (!strcmp(aTopic, "xpcom-shutdown")) { michael@0: rv = this->Deinit(); michael@0: } else { michael@0: // not reachable michael@0: rv = NS_ERROR_FAILURE; michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: // Handle GetUserMediaRequest, query available media device first. michael@0: nsresult michael@0: MediaPermissionManager::HandleRequest(nsRefPtr &req) michael@0: { michael@0: nsString callID; michael@0: req->GetCallID(callID); michael@0: michael@0: nsCOMPtr innerWindow = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(req->InnerWindowID())); michael@0: if (!innerWindow) { michael@0: MOZ_ASSERT(false, "No inner window"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: nsCOMPtr onSuccess = michael@0: new MediaDeviceSuccessCallback(req); michael@0: nsCOMPtr onError = michael@0: new MediaDeviceErrorCallback(callID); michael@0: michael@0: dom::MediaStreamConstraints constraints; michael@0: req->GetConstraints(constraints); michael@0: michael@0: nsRefPtr MediaMgr = MediaManager::GetInstance(); michael@0: nsresult rv = MediaMgr->GetUserMediaDevices(innerWindow, constraints, onSuccess, onError); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: } // namespace mozilla