diff options
Diffstat (limited to 'widget/gtk/MPRISServiceHandler.cpp')
-rw-r--r-- | widget/gtk/MPRISServiceHandler.cpp | 909 |
1 files changed, 909 insertions, 0 deletions
diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp new file mode 100644 index 0000000000..ae1ef8654d --- /dev/null +++ b/widget/gtk/MPRISServiceHandler.cpp @@ -0,0 +1,909 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "MPRISServiceHandler.h" + +#include <stdint.h> +#include <inttypes.h> +#include <unordered_map> + +#include "MPRISInterfaceDescription.h" +#include "mozilla/dom/MediaControlUtils.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "nsXULAppAPI.h" +#include "nsIXULAppInfo.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "WidgetUtilsGtk.h" +#include "AsyncDBus.h" +#include "prio.h" + +#define LOGMPRIS(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla { +namespace widget { + +// A global counter tracking the total images saved in the system and it will be +// used to form a unique image file name. +static uint32_t gImageNumber = 0; + +static inline Maybe<dom::MediaControlKey> GetMediaControlKey( + const gchar* aMethodName) { + const std::unordered_map<std::string, dom::MediaControlKey> map = { + {"Raise", dom::MediaControlKey::Focus}, + {"Next", dom::MediaControlKey::Nexttrack}, + {"Previous", dom::MediaControlKey::Previoustrack}, + {"Pause", dom::MediaControlKey::Pause}, + {"PlayPause", dom::MediaControlKey::Playpause}, + {"Stop", dom::MediaControlKey::Stop}, + {"Play", dom::MediaControlKey::Play}}; + + auto it = map.find(aMethodName); + return it == map.end() ? Nothing() : Some(it->second); +} + +static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aMethodName, GVariant* aParameters, + GDBusMethodInvocation* aInvocation, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + + Maybe<dom::MediaControlKey> key = GetMediaControlKey(aMethodName); + if (key.isNothing()) { + g_dbus_method_invocation_return_error( + aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, + "Method %s.%s.%s not supported", aObjectPath, aInterfaceName, + aMethodName); + return; + } + + MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData); + if (handler->PressKey(key.value())) { + g_dbus_method_invocation_return_value(aInvocation, nullptr); + } else { + g_dbus_method_invocation_return_error( + aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "%s.%s.%s is not available now", aObjectPath, aInterfaceName, + aMethodName); + } +} + +enum class Property : uint8_t { + eIdentity, + eDesktopEntry, + eHasTrackList, + eCanRaise, + eCanQuit, + eSupportedUriSchemes, + eSupportedMimeTypes, + eCanGoNext, + eCanGoPrevious, + eCanPlay, + eCanPause, + eCanSeek, + eCanControl, + eGetPlaybackStatus, + eGetMetadata, +}; + +static inline Maybe<dom::MediaControlKey> GetPairedKey(Property aProperty) { + switch (aProperty) { + case Property::eCanRaise: + return Some(dom::MediaControlKey::Focus); + case Property::eCanGoNext: + return Some(dom::MediaControlKey::Nexttrack); + case Property::eCanGoPrevious: + return Some(dom::MediaControlKey::Previoustrack); + case Property::eCanPlay: + return Some(dom::MediaControlKey::Play); + case Property::eCanPause: + return Some(dom::MediaControlKey::Pause); + default: + return Nothing(); + } +} + +static inline Maybe<Property> GetProperty(const gchar* aPropertyName) { + const std::unordered_map<std::string, Property> map = { + // org.mpris.MediaPlayer2 properties + {"Identity", Property::eIdentity}, + {"DesktopEntry", Property::eDesktopEntry}, + {"HasTrackList", Property::eHasTrackList}, + {"CanRaise", Property::eCanRaise}, + {"CanQuit", Property::eCanQuit}, + {"SupportedUriSchemes", Property::eSupportedUriSchemes}, + {"SupportedMimeTypes", Property::eSupportedMimeTypes}, + // org.mpris.MediaPlayer2.Player properties + {"CanGoNext", Property::eCanGoNext}, + {"CanGoPrevious", Property::eCanGoPrevious}, + {"CanPlay", Property::eCanPlay}, + {"CanPause", Property::eCanPause}, + {"CanSeek", Property::eCanSeek}, + {"CanControl", Property::eCanControl}, + {"PlaybackStatus", Property::eGetPlaybackStatus}, + {"Metadata", Property::eGetMetadata}}; + + auto it = map.find(aPropertyName); + return (it == map.end() ? Nothing() : Some(it->second)); +} + +static GVariant* HandleGetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GError** aError, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + + Maybe<Property> property = GetProperty(aPropertyName); + if (property.isNothing()) { + g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, + "%s.%s %s is not supported", aObjectPath, aInterfaceName, + aPropertyName); + return nullptr; + } + + MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData); + switch (property.value()) { + case Property::eSupportedUriSchemes: + case Property::eSupportedMimeTypes: + // No plan to implement OpenUri for now + return g_variant_new_strv(nullptr, 0); + case Property::eGetPlaybackStatus: + return handler->GetPlaybackStatus(); + case Property::eGetMetadata: + return handler->GetMetadataAsGVariant(); + case Property::eIdentity: + return g_variant_new_string(handler->Identity()); + case Property::eDesktopEntry: + return g_variant_new_string(handler->DesktopEntry()); + case Property::eHasTrackList: + case Property::eCanQuit: + case Property::eCanSeek: + return g_variant_new_boolean(false); + // Play/Pause would be blocked if CanControl is false + case Property::eCanControl: + return g_variant_new_boolean(true); + case Property::eCanRaise: + case Property::eCanGoNext: + case Property::eCanGoPrevious: + case Property::eCanPlay: + case Property::eCanPause: + Maybe<dom::MediaControlKey> key = GetPairedKey(property.value()); + MOZ_ASSERT(key.isSome()); + return g_variant_new_boolean(handler->IsMediaKeySupported(key.value())); + } + + MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete"); + return nullptr; +} + +static gboolean HandleSetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GVariant* aValue, + GError** aError, gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s:%s setting is not supported", aInterfaceName, aPropertyName); + return false; +} + +static const GDBusInterfaceVTable gInterfaceVTable = { + HandleMethodCall, HandleGetProperty, HandleSetProperty}; + +void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection, + aName); +} + +void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName); +} + +void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection, + aName); +} + +void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection, + const gchar* aName) { + LOGMPRIS("OnNameAcquired: %s", aName); + mConnection = aConnection; +} + +void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection, + const gchar* aName) { + LOGMPRIS("OnNameLost: %s", aName); + mConnection = nullptr; + if (!mRootRegistrationId) { + return; + } + + if (!aConnection) { + return; + } + + if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) { + mRootRegistrationId = 0; + } else { + // Note: Most code examples in the internet probably dont't even check the + // result here, but + // according to the spec it _can_ return false. + LOGMPRIS("Unable to unregister root object from within onNameLost!"); + } + + if (!mPlayerRegistrationId) { + return; + } + + if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) { + mPlayerRegistrationId = 0; + } else { + // Note: Most code examples in the internet probably dont't even check the + // result here, but + // according to the spec it _can_ return false. + LOGMPRIS("Unable to unregister object from within onNameLost!"); + } +} + +void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection, + const gchar* aName) { + GUniquePtr<GError> error; + LOGMPRIS("OnBusAcquired: %s", aName); + + mRootRegistrationId = g_dbus_connection_register_object( + aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0], + &gInterfaceVTable, this, /* user_data */ + nullptr, /* user_data_free_func */ + getter_Transfers(error)); /* GError** */ + + if (mRootRegistrationId == 0) { + LOGMPRIS("Failed at root registration: %s", + error ? error->message : "Unknown Error"); + return; + } + + mPlayerRegistrationId = g_dbus_connection_register_object( + aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1], + &gInterfaceVTable, this, /* user_data */ + nullptr, /* user_data_free_func */ + getter_Transfers(error)); /* GError** */ + + if (mPlayerRegistrationId == 0) { + LOGMPRIS("Failed at object registration: %s", + error ? error->message : "Unknown Error"); + } +} + +void MPRISServiceHandler::SetServiceName(const char* aName) { + nsCString dbusName(aName); + dbusName.ReplaceChar(':', '_'); + dbusName.ReplaceChar('.', '_'); + mServiceName = + nsCString(DBUS_MPRIS_SERVICE_NAME) + nsCString(".instance") + dbusName; +} + +const char* MPRISServiceHandler::GetServiceName() { return mServiceName.get(); } + +/* static */ +void g_bus_get_callback(GObject* aSourceObject, GAsyncResult* aRes, + gpointer aUserData) { + GUniquePtr<GError> error; + + GDBusConnection* conn = g_bus_get_finish(aRes, getter_Transfers(error)); + if (!conn) { + if (!IsCancelledGError(error.get())) { + NS_WARNING(nsPrintfCString("Failure at g_bus_get_finish: %s", + error ? error->message : "Unknown Error") + .get()); + } + return; + } + + MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData); + if (!handler) { + NS_WARNING( + nsPrintfCString("Failure to get a MPRISServiceHandler*: %p", handler) + .get()); + return; + } + + handler->OwnName(conn); +} + +void MPRISServiceHandler::OwnName(GDBusConnection* aConnection) { + MOZ_ASSERT(NS_IsMainThread()); + + SetServiceName(g_dbus_connection_get_unique_name(aConnection)); + + GUniquePtr<GError> error; + + InitIdentity(); + mOwnerId = g_bus_own_name_on_connection( + aConnection, GetServiceName(), + // Enter a waiting queue until this service name is free + // (likely another FF instance is running/has been crashed) + G_BUS_NAME_OWNER_FLAGS_NONE, OnNameAcquiredStatic, OnNameLostStatic, this, + nullptr); + + /* parse introspection data */ + mIntrospectionData = dont_AddRef( + g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error))); + + if (!mIntrospectionData) { + LOGMPRIS("Failed at parsing XML Interface definition: %s", + error ? error->message : "Unknown Error"); + return; + } + + OnBusAcquired(aConnection, GetServiceName()); +} + +bool MPRISServiceHandler::Open() { + MOZ_ASSERT(!mInitialized); + MOZ_ASSERT(NS_IsMainThread()); + + mDBusGetCancellable = dont_AddRef(g_cancellable_new()); + g_bus_get(G_BUS_TYPE_SESSION, mDBusGetCancellable, g_bus_get_callback, this); + + mInitialized = true; + return true; +} + +MPRISServiceHandler::MPRISServiceHandler() = default; +MPRISServiceHandler::~MPRISServiceHandler() { + MOZ_ASSERT(!mInitialized, "Close hasn't been called!"); +} + +void MPRISServiceHandler::Close() { + // Reset playback state and metadata before disconnect from dbus. + SetPlaybackState(dom::MediaSessionPlaybackState::None); + ClearMetadata(); + + OnNameLost(mConnection, GetServiceName()); + + if (mDBusGetCancellable) { + g_cancellable_cancel(mDBusGetCancellable); + mDBusGetCancellable = nullptr; + } + + if (mOwnerId != 0) { + g_bus_unown_name(mOwnerId); + } + + mIntrospectionData = nullptr; + + mInitialized = false; + MediaControlKeySource::Close(); +} + +bool MPRISServiceHandler::IsOpened() const { return mInitialized; } + +void MPRISServiceHandler::InitIdentity() { + nsresult rv; + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1", &rv); + + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = appInfo->GetVendor(mIdentity); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = appInfo->GetName(mDesktopEntry); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mIdentity.Append(' '); + mIdentity.Append(mDesktopEntry); + + // Compute the desktop entry name like nsAppRunner does for g_set_prgname + ToLowerCase(mDesktopEntry); +} + +const char* MPRISServiceHandler::Identity() const { + NS_WARNING_ASSERTION(mInitialized, + "MPRISServiceHandler should have been initialized."); + return mIdentity.get(); +} + +const char* MPRISServiceHandler::DesktopEntry() const { + NS_WARNING_ASSERTION(mInitialized, + "MPRISServiceHandler should have been initialized."); + return mDesktopEntry.get(); +} + +bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const { + MOZ_ASSERT(mInitialized); + if (!IsMediaKeySupported(aKey)) { + LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey)); + return false; + } + LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey)); + EmitEvent(aKey); + return true; +} + +void MPRISServiceHandler::SetPlaybackState( + dom::MediaSessionPlaybackState aState) { + LOGMPRIS("SetPlaybackState"); + if (mPlaybackState == aState) { + return; + } + + MediaControlKeySource::SetPlaybackState(aState); + + GVariant* state = GetPlaybackStatus(); + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); + + LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'"); + Unused << EmitPropertiesChangedSignal(parameters); +} + +GVariant* MPRISServiceHandler::GetPlaybackStatus() const { + switch (GetPlaybackState()) { + case dom::MediaSessionPlaybackState::Playing: + return g_variant_new_string("Playing"); + case dom::MediaSessionPlaybackState::Paused: + return g_variant_new_string("Paused"); + case dom::MediaSessionPlaybackState::None: + return g_variant_new_string("Stopped"); + default: + MOZ_ASSERT_UNREACHABLE("Invalid Playback State"); + return nullptr; + } +} + +void MPRISServiceHandler::SetMediaMetadata( + const dom::MediaMetadataBase& aMetadata) { + // Reset the index of the next available image to be fetched in the artwork, + // before checking the fetching process should be started or not. The image + // fetching process could be skipped if the image being fetching currently is + // in the artwork. If the current image fetching fails, the next availabe + // candidate should be the first image in the latest artwork + mNextImageIndex = 0; + + // No need to fetch a MPRIS image if + // 1) MPRIS image is being fetched, and the one in fetching is in the artwork + // 2) MPRIS image is not being fetched, and the one in use is in the artwork + if (!mFetchingUrl.IsEmpty()) { + if (dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) { + LOGMPRIS( + "No need to load MPRIS image. The one being processed is in the " + "artwork"); + // Set MPRIS without the image first. The image will be loaded to MPRIS + // asynchronously once it's fetched and saved into a local file + SetMediaMetadataInternal(aMetadata); + return; + } + } else if (!mCurrentImageUrl.IsEmpty()) { + if (dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) { + LOGMPRIS("No need to load MPRIS image. The one in use is in the artwork"); + SetMediaMetadataInternal(aMetadata, false); + return; + } + } + + // Set MPRIS without the image first then load the image to MPRIS + // asynchronously + SetMediaMetadataInternal(aMetadata); + LoadImageAtIndex(mNextImageIndex++); +} + +bool MPRISServiceHandler::EmitMetadataChanged() const { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant()); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); + + LOGMPRIS("Emit MPRIS property changes for 'Metadata'"); + return EmitPropertiesChangedSignal(parameters); +} + +void MPRISServiceHandler::SetMediaMetadataInternal( + const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) { + mMPRISMetadata.UpdateFromMetadataBase(aMetadata); + if (aClearArtUrl) { + mMPRISMetadata.mArtUrl.Truncate(); + } + EmitMetadataChanged(); +} + +void MPRISServiceHandler::ClearMetadata() { + mMPRISMetadata.Clear(); + mImageFetchRequest.DisconnectIfExists(); + RemoveAllLocalImages(); + mCurrentImageUrl.Truncate(); + mFetchingUrl.Truncate(); + mNextImageIndex = 0; + mSupportedKeys = 0; + EmitMetadataChanged(); +} + +void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aIndex >= mMPRISMetadata.mArtwork.Length()) { + LOGMPRIS("Stop loading image to MPRIS. No available image"); + mImageFetchRequest.DisconnectIfExists(); + return; + } + + const dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex]; + + if (!dom::IsValidImageUrl(image.mSrc)) { + LOGMPRIS("Skip the image with invalid URL. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + mImageFetchRequest.DisconnectIfExists(); + mFetchingUrl = image.mSrc; + + mImageFetcher = MakeUnique<dom::FetchImageHelper>(image); + RefPtr<MPRISServiceHandler> self = this; + mImageFetcher->FetchImage() + ->Then( + AbstractThread::MainThread(), __func__, + [this, self](const nsCOMPtr<imgIContainer>& aImage) { + LOGMPRIS("The image is fetched successfully"); + mImageFetchRequest.Complete(); + + uint32_t size = 0; + char* data = nullptr; + // Only used to hold the image data + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = dom::GetEncodedImageBuffer( + aImage, mMimeType, getter_AddRefs(inputStream), &size, &data); + if (NS_FAILED(rv) || !inputStream || size == 0 || !data) { + LOGMPRIS("Failed to get the image buffer info. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + if (SetImageToDisplay(data, size)) { + mCurrentImageUrl = mFetchingUrl; + LOGMPRIS("The MPRIS image is updated to the image from: %s", + NS_ConvertUTF16toUTF8(mCurrentImageUrl).get()); + } else { + LOGMPRIS("Failed to set image to MPRIS"); + mCurrentImageUrl.Truncate(); + } + + mFetchingUrl.Truncate(); + }, + [this, self](bool) { + LOGMPRIS("Failed to fetch image. Try next image"); + mImageFetchRequest.Complete(); + mFetchingUrl.Truncate(); + LoadImageAtIndex(mNextImageIndex++); + }) + ->Track(mImageFetchRequest); +} + +bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData, + uint32_t aDataSize) { + if (!RenewLocalImageFile(aImageData, aDataSize)) { + return false; + } + MOZ_ASSERT(mLocalImageFile); + + mMPRISMetadata.mArtUrl = nsCString("file://"); + mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath()); + + LOGMPRIS("The image file is created at %s", mMPRISMetadata.mArtUrl.get()); + return EmitMetadataChanged(); +} + +bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData, + uint32_t aDataSize) { + MOZ_ASSERT(aImageData); + MOZ_ASSERT(aDataSize != 0); + + if (!InitLocalImageFile()) { + LOGMPRIS("Failed to create a new image"); + return false; + } + + MOZ_ASSERT(mLocalImageFile); + nsCOMPtr<nsIOutputStream> out; + NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile, + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + uint32_t written; + nsresult rv = out->Write(aImageData, aDataSize, &written); + if (NS_FAILED(rv) || written != aDataSize) { + LOGMPRIS("Failed to write an image file"); + RemoveAllLocalImages(); + return false; + } + + return true; +} + +static const char* GetImageFileExtension(const char* aMimeType) { + MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0); + return "png"; +} + +bool MPRISServiceHandler::InitLocalImageFile() { + RemoveAllLocalImages(); + + if (!InitLocalImageFolder()) { + return false; + } + + MOZ_ASSERT(mLocalImageFolder); + MOZ_ASSERT(!mLocalImageFile); + nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile)); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to get the image folder"); + return false; + } + + auto cleanup = + MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] { + mLocalImageFile = nullptr; + }); + + // Create an unique file name to work around the file caching mechanism in the + // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's + // MPRIS, this pair will be cached. As long as the filename is same, even the + // file content specified by Y is changed to Z, the image will stay unchanged. + // The image shown in the Ubuntu's notification is still X instead of Z. + // Changing the filename constantly works around this problem + char filename[64]; + SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++, + GetImageFileExtension(mMimeType.get())); + + rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename)); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to create an image filename"); + return false; + } + + rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to create an image file"); + return false; + } + + cleanup.release(); + return true; +} + +bool MPRISServiceHandler::InitLocalImageFolder() { + if (mLocalImageFolder && LocalImageFolderExists()) { + return true; + } + + nsresult rv = NS_ERROR_FAILURE; + if (IsRunningUnderFlatpak()) { + // The XDG_DATA_HOME points to the same location in the host and guest + // filesystem. + if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) { + rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true, + getter_AddRefs(mLocalImageFolder)); + } + } else { + rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR, + getter_AddRefs(mLocalImageFolder)); + } + + if (NS_FAILED(rv) || !mLocalImageFolder) { + LOGMPRIS("Failed to get the image folder"); + return false; + } + + auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; }); + + rv = mLocalImageFolder->Append(u"firefox-mpris"_ns); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to name an image folder"); + return false; + } + + if (!LocalImageFolderExists()) { + rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to create an image folder"); + return false; + } + } + + cleanup.release(); + return true; +} + +void MPRISServiceHandler::RemoveAllLocalImages() { + if (!mLocalImageFolder || !LocalImageFolderExists()) { + return; + } + + nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true); + if (NS_FAILED(rv)) { + // It's ok to fail. The next removal is called when updating the + // media-session image, or closing the MPRIS. + LOGMPRIS("Failed to remove images"); + } + + LOGMPRIS("Abandon %s", + mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing"); + mMPRISMetadata.mArtUrl.Truncate(); + mLocalImageFile = nullptr; + mLocalImageFolder = nullptr; +} + +bool MPRISServiceHandler::LocalImageFolderExists() { + MOZ_ASSERT(mLocalImageFolder); + + bool exists; + nsresult rv = mLocalImageFolder->Exists(&exists); + return NS_SUCCEEDED(rv) && exists; +} + +GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "mpris:trackid", + g_variant_new("o", DBUS_MPRIS_TRACK_PATH)); + + g_variant_builder_add( + &builder, "{sv}", "xesam:title", + g_variant_new_string(static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get()))); + + g_variant_builder_add( + &builder, "{sv}", "xesam:album", + g_variant_new_string(static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get()))); + + GVariantBuilder artistBuilder; + g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as")); + g_variant_builder_add( + &artistBuilder, "s", + static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get())); + g_variant_builder_add(&builder, "{sv}", "xesam:artist", + g_variant_builder_end(&artistBuilder)); + + if (!mMPRISMetadata.mArtUrl.IsEmpty()) { + g_variant_builder_add(&builder, "{sv}", "mpris:artUrl", + g_variant_new_string(static_cast<const gchar*>( + mMPRISMetadata.mArtUrl.get()))); + } + + return g_variant_builder_end(&builder); +} + +void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey) const { + for (const auto& listener : mListeners) { + listener->OnActionPerformed(dom::MediaControlAction(aKey)); + } +} + +struct InterfaceProperty { + const char* interface; + const char* property; +}; +static const std::unordered_map<dom::MediaControlKey, InterfaceProperty> + gKeyProperty = { + {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}}, + {dom::MediaControlKey::Nexttrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}}, + {dom::MediaControlKey::Previoustrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}}, + {dom::MediaControlKey::Play, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}}, + {dom::MediaControlKey::Pause, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}}; + +void MPRISServiceHandler::SetSupportedMediaKeys( + const MediaKeysArray& aSupportedKeys) { + uint32_t supportedKeys = 0; + for (const dom::MediaControlKey& key : aSupportedKeys) { + supportedKeys |= GetMediaKeyMask(key); + } + + if (mSupportedKeys == supportedKeys) { + LOGMPRIS("Supported keys stay the same"); + return; + } + + uint32_t oldSupportedKeys = mSupportedKeys; + mSupportedKeys = supportedKeys; + + // Emit related property changes + for (auto it : gKeyProperty) { + bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first); + bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first); + if (keyWasSupported != keyIsSupported) { + LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface, + it.second.property, keyIsSupported ? "true" : "false"); + EmitSupportedKeyChanged(it.first, keyIsSupported); + } + } +} + +bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const { + return mSupportedKeys & GetMediaKeyMask(aKey); +} + +bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey, + bool aSupported) const { + auto it = gKeyProperty.find(aKey); + if (it == gKeyProperty.end()) { + LOGMPRIS("No property for %s", ToMediaControlKeyStr(aKey)); + return false; + } + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", + static_cast<const gchar*>(it->second.property), + g_variant_new_boolean(aSupported)); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", static_cast<const gchar*>(it->second.interface), &builder, + nullptr); + + LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface, + it->second.property); + return EmitPropertiesChangedSignal(parameters); +} + +bool MPRISServiceHandler::EmitPropertiesChangedSignal( + GVariant* aParameters) const { + if (!mConnection) { + LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal"); + return false; + } + + GError* error = nullptr; + if (!g_dbus_connection_emit_signal( + mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH, + "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters, + &error)) { + LOGMPRIS("Failed to emit MPRIS property changes: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + return false; + } + + return true; +} + +#undef LOGMPRIS + +} // namespace widget +} // namespace mozilla |