summaryrefslogtreecommitdiffstats
path: root/widget/gtk/MPRISServiceHandler.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /widget/gtk/MPRISServiceHandler.cpp
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/gtk/MPRISServiceHandler.cpp')
-rw-r--r--widget/gtk/MPRISServiceHandler.cpp856
1 files changed, 856 insertions, 0 deletions
diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp
new file mode 100644
index 0000000000..4080b56c17
--- /dev/null
+++ b/widget/gtk/MPRISServiceHandler.cpp
@@ -0,0 +1,856 @@
+/* -*- 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 "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 (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");
+ }
+}
+
+bool MPRISServiceHandler::Open() {
+ MOZ_ASSERT(!mInitialized);
+ MOZ_ASSERT(NS_IsMainThread());
+ GUniquePtr<GError> error;
+ gchar serviceName[256];
+
+ InitIdentity();
+ SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid());
+ mOwnerId =
+ g_bus_own_name(G_BUS_TYPE_SESSION, serviceName,
+ // 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, OnBusAcquiredStatic,
+ 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 false;
+ }
+
+ mInitialized = true;
+ return true;
+}
+
+MPRISServiceHandler::MPRISServiceHandler() = default;
+MPRISServiceHandler::~MPRISServiceHandler() {
+ MOZ_ASSERT(!mInitialized, "Close hasn't been called!");
+}
+
+void MPRISServiceHandler::Close() {
+ gchar serviceName[256];
+ SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid());
+
+ // Reset playback state and metadata before disconnect from dbus.
+ SetPlaybackState(dom::MediaSessionPlaybackState::None);
+ ClearMetadata();
+
+ OnNameLost(mConnection, serviceName);
+
+ 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