Bug 1822331 [wpt PR 38990] - [@scope] Enable implicit :scope and relative selectors...
[gecko.git] / widget / gtk / MPRISServiceHandler.cpp
blob4080b56c17998fd034513ec646a0d11a28835fb3
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "MPRISServiceHandler.h"
9 #include <stdint.h>
10 #include <inttypes.h>
11 #include <unordered_map>
13 #include "MPRISInterfaceDescription.h"
14 #include "mozilla/dom/MediaControlUtils.h"
15 #include "mozilla/GRefPtr.h"
16 #include "mozilla/GUniquePtr.h"
17 #include "mozilla/UniquePtrExtensions.h"
18 #include "mozilla/Maybe.h"
19 #include "mozilla/ScopeExit.h"
20 #include "mozilla/Sprintf.h"
21 #include "nsXULAppAPI.h"
22 #include "nsIXULAppInfo.h"
23 #include "nsIOutputStream.h"
24 #include "nsNetUtil.h"
25 #include "nsServiceManagerUtils.h"
26 #include "WidgetUtilsGtk.h"
27 #include "prio.h"
29 #define LOGMPRIS(msg, ...) \
30 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
31 ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))
33 namespace mozilla {
34 namespace widget {
36 // A global counter tracking the total images saved in the system and it will be
37 // used to form a unique image file name.
38 static uint32_t gImageNumber = 0;
40 static inline Maybe<dom::MediaControlKey> GetMediaControlKey(
41 const gchar* aMethodName) {
42 const std::unordered_map<std::string, dom::MediaControlKey> map = {
43 {"Raise", dom::MediaControlKey::Focus},
44 {"Next", dom::MediaControlKey::Nexttrack},
45 {"Previous", dom::MediaControlKey::Previoustrack},
46 {"Pause", dom::MediaControlKey::Pause},
47 {"PlayPause", dom::MediaControlKey::Playpause},
48 {"Stop", dom::MediaControlKey::Stop},
49 {"Play", dom::MediaControlKey::Play}};
51 auto it = map.find(aMethodName);
52 return it == map.end() ? Nothing() : Some(it->second);
55 static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender,
56 const gchar* aObjectPath,
57 const gchar* aInterfaceName,
58 const gchar* aMethodName, GVariant* aParameters,
59 GDBusMethodInvocation* aInvocation,
60 gpointer aUserData) {
61 MOZ_ASSERT(aUserData);
62 MOZ_ASSERT(NS_IsMainThread());
64 Maybe<dom::MediaControlKey> key = GetMediaControlKey(aMethodName);
65 if (key.isNothing()) {
66 g_dbus_method_invocation_return_error(
67 aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
68 "Method %s.%s.%s not supported", aObjectPath, aInterfaceName,
69 aMethodName);
70 return;
73 MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
74 if (handler->PressKey(key.value())) {
75 g_dbus_method_invocation_return_value(aInvocation, nullptr);
76 } else {
77 g_dbus_method_invocation_return_error(
78 aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
79 "%s.%s.%s is not available now", aObjectPath, aInterfaceName,
80 aMethodName);
84 enum class Property : uint8_t {
85 eIdentity,
86 eDesktopEntry,
87 eHasTrackList,
88 eCanRaise,
89 eCanQuit,
90 eSupportedUriSchemes,
91 eSupportedMimeTypes,
92 eCanGoNext,
93 eCanGoPrevious,
94 eCanPlay,
95 eCanPause,
96 eCanSeek,
97 eCanControl,
98 eGetPlaybackStatus,
99 eGetMetadata,
102 static inline Maybe<dom::MediaControlKey> GetPairedKey(Property aProperty) {
103 switch (aProperty) {
104 case Property::eCanRaise:
105 return Some(dom::MediaControlKey::Focus);
106 case Property::eCanGoNext:
107 return Some(dom::MediaControlKey::Nexttrack);
108 case Property::eCanGoPrevious:
109 return Some(dom::MediaControlKey::Previoustrack);
110 case Property::eCanPlay:
111 return Some(dom::MediaControlKey::Play);
112 case Property::eCanPause:
113 return Some(dom::MediaControlKey::Pause);
114 default:
115 return Nothing();
119 static inline Maybe<Property> GetProperty(const gchar* aPropertyName) {
120 const std::unordered_map<std::string, Property> map = {
121 // org.mpris.MediaPlayer2 properties
122 {"Identity", Property::eIdentity},
123 {"DesktopEntry", Property::eDesktopEntry},
124 {"HasTrackList", Property::eHasTrackList},
125 {"CanRaise", Property::eCanRaise},
126 {"CanQuit", Property::eCanQuit},
127 {"SupportedUriSchemes", Property::eSupportedUriSchemes},
128 {"SupportedMimeTypes", Property::eSupportedMimeTypes},
129 // org.mpris.MediaPlayer2.Player properties
130 {"CanGoNext", Property::eCanGoNext},
131 {"CanGoPrevious", Property::eCanGoPrevious},
132 {"CanPlay", Property::eCanPlay},
133 {"CanPause", Property::eCanPause},
134 {"CanSeek", Property::eCanSeek},
135 {"CanControl", Property::eCanControl},
136 {"PlaybackStatus", Property::eGetPlaybackStatus},
137 {"Metadata", Property::eGetMetadata}};
139 auto it = map.find(aPropertyName);
140 return (it == map.end() ? Nothing() : Some(it->second));
143 static GVariant* HandleGetProperty(GDBusConnection* aConnection,
144 const gchar* aSender,
145 const gchar* aObjectPath,
146 const gchar* aInterfaceName,
147 const gchar* aPropertyName, GError** aError,
148 gpointer aUserData) {
149 MOZ_ASSERT(aUserData);
150 MOZ_ASSERT(NS_IsMainThread());
152 Maybe<Property> property = GetProperty(aPropertyName);
153 if (property.isNothing()) {
154 g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
155 "%s.%s %s is not supported", aObjectPath, aInterfaceName,
156 aPropertyName);
157 return nullptr;
160 MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
161 switch (property.value()) {
162 case Property::eSupportedUriSchemes:
163 case Property::eSupportedMimeTypes:
164 // No plan to implement OpenUri for now
165 return g_variant_new_strv(nullptr, 0);
166 case Property::eGetPlaybackStatus:
167 return handler->GetPlaybackStatus();
168 case Property::eGetMetadata:
169 return handler->GetMetadataAsGVariant();
170 case Property::eIdentity:
171 return g_variant_new_string(handler->Identity());
172 case Property::eDesktopEntry:
173 return g_variant_new_string(handler->DesktopEntry());
174 case Property::eHasTrackList:
175 case Property::eCanQuit:
176 case Property::eCanSeek:
177 return g_variant_new_boolean(false);
178 // Play/Pause would be blocked if CanControl is false
179 case Property::eCanControl:
180 return g_variant_new_boolean(true);
181 case Property::eCanRaise:
182 case Property::eCanGoNext:
183 case Property::eCanGoPrevious:
184 case Property::eCanPlay:
185 case Property::eCanPause:
186 Maybe<dom::MediaControlKey> key = GetPairedKey(property.value());
187 MOZ_ASSERT(key.isSome());
188 return g_variant_new_boolean(handler->IsMediaKeySupported(key.value()));
191 MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete");
192 return nullptr;
195 static gboolean HandleSetProperty(GDBusConnection* aConnection,
196 const gchar* aSender,
197 const gchar* aObjectPath,
198 const gchar* aInterfaceName,
199 const gchar* aPropertyName, GVariant* aValue,
200 GError** aError, gpointer aUserData) {
201 MOZ_ASSERT(aUserData);
202 MOZ_ASSERT(NS_IsMainThread());
203 g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
204 "%s:%s setting is not supported", aInterfaceName, aPropertyName);
205 return false;
208 static const GDBusInterfaceVTable gInterfaceVTable = {
209 HandleMethodCall, HandleGetProperty, HandleSetProperty};
211 void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection,
212 const gchar* aName,
213 gpointer aUserData) {
214 MOZ_ASSERT(aUserData);
215 static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection,
216 aName);
219 void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection,
220 const gchar* aName,
221 gpointer aUserData) {
222 MOZ_ASSERT(aUserData);
223 static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName);
226 void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection,
227 const gchar* aName,
228 gpointer aUserData) {
229 MOZ_ASSERT(aUserData);
230 static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection,
231 aName);
234 void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection,
235 const gchar* aName) {
236 LOGMPRIS("OnNameAcquired: %s", aName);
237 mConnection = aConnection;
240 void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection,
241 const gchar* aName) {
242 LOGMPRIS("OnNameLost: %s", aName);
243 mConnection = nullptr;
244 if (!mRootRegistrationId) {
245 return;
248 if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) {
249 mRootRegistrationId = 0;
250 } else {
251 // Note: Most code examples in the internet probably dont't even check the
252 // result here, but
253 // according to the spec it _can_ return false.
254 LOGMPRIS("Unable to unregister root object from within onNameLost!");
257 if (!mPlayerRegistrationId) {
258 return;
261 if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) {
262 mPlayerRegistrationId = 0;
263 } else {
264 // Note: Most code examples in the internet probably dont't even check the
265 // result here, but
266 // according to the spec it _can_ return false.
267 LOGMPRIS("Unable to unregister object from within onNameLost!");
271 void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection,
272 const gchar* aName) {
273 GUniquePtr<GError> error;
274 LOGMPRIS("OnBusAcquired: %s", aName);
276 mRootRegistrationId = g_dbus_connection_register_object(
277 aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0],
278 &gInterfaceVTable, this, /* user_data */
279 nullptr, /* user_data_free_func */
280 getter_Transfers(error)); /* GError** */
282 if (mRootRegistrationId == 0) {
283 LOGMPRIS("Failed at root registration: %s",
284 error ? error->message : "Unknown Error");
285 return;
288 mPlayerRegistrationId = g_dbus_connection_register_object(
289 aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1],
290 &gInterfaceVTable, this, /* user_data */
291 nullptr, /* user_data_free_func */
292 getter_Transfers(error)); /* GError** */
294 if (mPlayerRegistrationId == 0) {
295 LOGMPRIS("Failed at object registration: %s",
296 error ? error->message : "Unknown Error");
300 bool MPRISServiceHandler::Open() {
301 MOZ_ASSERT(!mInitialized);
302 MOZ_ASSERT(NS_IsMainThread());
303 GUniquePtr<GError> error;
304 gchar serviceName[256];
306 InitIdentity();
307 SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid());
308 mOwnerId =
309 g_bus_own_name(G_BUS_TYPE_SESSION, serviceName,
310 // Enter a waiting queue until this service name is free
311 // (likely another FF instance is running/has been crashed)
312 G_BUS_NAME_OWNER_FLAGS_NONE, OnBusAcquiredStatic,
313 OnNameAcquiredStatic, OnNameLostStatic, this, nullptr);
315 /* parse introspection data */
316 mIntrospectionData = dont_AddRef(
317 g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error)));
319 if (!mIntrospectionData) {
320 LOGMPRIS("Failed at parsing XML Interface definition: %s",
321 error ? error->message : "Unknown Error");
322 return false;
325 mInitialized = true;
326 return true;
329 MPRISServiceHandler::MPRISServiceHandler() = default;
330 MPRISServiceHandler::~MPRISServiceHandler() {
331 MOZ_ASSERT(!mInitialized, "Close hasn't been called!");
334 void MPRISServiceHandler::Close() {
335 gchar serviceName[256];
336 SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid());
338 // Reset playback state and metadata before disconnect from dbus.
339 SetPlaybackState(dom::MediaSessionPlaybackState::None);
340 ClearMetadata();
342 OnNameLost(mConnection, serviceName);
344 if (mOwnerId != 0) {
345 g_bus_unown_name(mOwnerId);
348 mIntrospectionData = nullptr;
350 mInitialized = false;
351 MediaControlKeySource::Close();
354 bool MPRISServiceHandler::IsOpened() const { return mInitialized; }
356 void MPRISServiceHandler::InitIdentity() {
357 nsresult rv;
358 nsCOMPtr<nsIXULAppInfo> appInfo =
359 do_GetService("@mozilla.org/xre/app-info;1", &rv);
361 MOZ_ASSERT(NS_SUCCEEDED(rv));
362 rv = appInfo->GetVendor(mIdentity);
363 MOZ_ASSERT(NS_SUCCEEDED(rv));
364 rv = appInfo->GetName(mDesktopEntry);
365 MOZ_ASSERT(NS_SUCCEEDED(rv));
367 mIdentity.Append(' ');
368 mIdentity.Append(mDesktopEntry);
370 // Compute the desktop entry name like nsAppRunner does for g_set_prgname
371 ToLowerCase(mDesktopEntry);
374 const char* MPRISServiceHandler::Identity() const {
375 NS_WARNING_ASSERTION(mInitialized,
376 "MPRISServiceHandler should have been initialized.");
377 return mIdentity.get();
380 const char* MPRISServiceHandler::DesktopEntry() const {
381 NS_WARNING_ASSERTION(mInitialized,
382 "MPRISServiceHandler should have been initialized.");
383 return mDesktopEntry.get();
386 bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const {
387 MOZ_ASSERT(mInitialized);
388 if (!IsMediaKeySupported(aKey)) {
389 LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey));
390 return false;
392 LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey));
393 EmitEvent(aKey);
394 return true;
397 void MPRISServiceHandler::SetPlaybackState(
398 dom::MediaSessionPlaybackState aState) {
399 LOGMPRIS("SetPlaybackState");
400 if (mPlaybackState == aState) {
401 return;
404 MediaControlKeySource::SetPlaybackState(aState);
406 GVariant* state = GetPlaybackStatus();
407 GVariantBuilder builder;
408 g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
409 g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state);
411 GVariant* parameters = g_variant_new(
412 "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);
414 LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'");
415 Unused << EmitPropertiesChangedSignal(parameters);
418 GVariant* MPRISServiceHandler::GetPlaybackStatus() const {
419 switch (GetPlaybackState()) {
420 case dom::MediaSessionPlaybackState::Playing:
421 return g_variant_new_string("Playing");
422 case dom::MediaSessionPlaybackState::Paused:
423 return g_variant_new_string("Paused");
424 case dom::MediaSessionPlaybackState::None:
425 return g_variant_new_string("Stopped");
426 default:
427 MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
428 return nullptr;
432 void MPRISServiceHandler::SetMediaMetadata(
433 const dom::MediaMetadataBase& aMetadata) {
434 // Reset the index of the next available image to be fetched in the artwork,
435 // before checking the fetching process should be started or not. The image
436 // fetching process could be skipped if the image being fetching currently is
437 // in the artwork. If the current image fetching fails, the next availabe
438 // candidate should be the first image in the latest artwork
439 mNextImageIndex = 0;
441 // No need to fetch a MPRIS image if
442 // 1) MPRIS image is being fetched, and the one in fetching is in the artwork
443 // 2) MPRIS image is not being fetched, and the one in use is in the artwork
444 if (!mFetchingUrl.IsEmpty()) {
445 if (dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) {
446 LOGMPRIS(
447 "No need to load MPRIS image. The one being processed is in the "
448 "artwork");
449 // Set MPRIS without the image first. The image will be loaded to MPRIS
450 // asynchronously once it's fetched and saved into a local file
451 SetMediaMetadataInternal(aMetadata);
452 return;
454 } else if (!mCurrentImageUrl.IsEmpty()) {
455 if (dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) {
456 LOGMPRIS("No need to load MPRIS image. The one in use is in the artwork");
457 SetMediaMetadataInternal(aMetadata, false);
458 return;
462 // Set MPRIS without the image first then load the image to MPRIS
463 // asynchronously
464 SetMediaMetadataInternal(aMetadata);
465 LoadImageAtIndex(mNextImageIndex++);
468 bool MPRISServiceHandler::EmitMetadataChanged() const {
469 GVariantBuilder builder;
470 g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
471 g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant());
473 GVariant* parameters = g_variant_new(
474 "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);
476 LOGMPRIS("Emit MPRIS property changes for 'Metadata'");
477 return EmitPropertiesChangedSignal(parameters);
480 void MPRISServiceHandler::SetMediaMetadataInternal(
481 const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) {
482 mMPRISMetadata.UpdateFromMetadataBase(aMetadata);
483 if (aClearArtUrl) {
484 mMPRISMetadata.mArtUrl.Truncate();
486 EmitMetadataChanged();
489 void MPRISServiceHandler::ClearMetadata() {
490 mMPRISMetadata.Clear();
491 mImageFetchRequest.DisconnectIfExists();
492 RemoveAllLocalImages();
493 mCurrentImageUrl.Truncate();
494 mFetchingUrl.Truncate();
495 mNextImageIndex = 0;
496 mSupportedKeys = 0;
497 EmitMetadataChanged();
500 void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex) {
501 MOZ_ASSERT(NS_IsMainThread());
503 if (aIndex >= mMPRISMetadata.mArtwork.Length()) {
504 LOGMPRIS("Stop loading image to MPRIS. No available image");
505 mImageFetchRequest.DisconnectIfExists();
506 return;
509 const dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex];
511 if (!dom::IsValidImageUrl(image.mSrc)) {
512 LOGMPRIS("Skip the image with invalid URL. Try next image");
513 LoadImageAtIndex(mNextImageIndex++);
514 return;
517 mImageFetchRequest.DisconnectIfExists();
518 mFetchingUrl = image.mSrc;
520 mImageFetcher = MakeUnique<dom::FetchImageHelper>(image);
521 RefPtr<MPRISServiceHandler> self = this;
522 mImageFetcher->FetchImage()
523 ->Then(
524 AbstractThread::MainThread(), __func__,
525 [this, self](const nsCOMPtr<imgIContainer>& aImage) {
526 LOGMPRIS("The image is fetched successfully");
527 mImageFetchRequest.Complete();
529 uint32_t size = 0;
530 char* data = nullptr;
531 // Only used to hold the image data
532 nsCOMPtr<nsIInputStream> inputStream;
533 nsresult rv = dom::GetEncodedImageBuffer(
534 aImage, mMimeType, getter_AddRefs(inputStream), &size, &data);
535 if (NS_FAILED(rv) || !inputStream || size == 0 || !data) {
536 LOGMPRIS("Failed to get the image buffer info. Try next image");
537 LoadImageAtIndex(mNextImageIndex++);
538 return;
541 if (SetImageToDisplay(data, size)) {
542 mCurrentImageUrl = mFetchingUrl;
543 LOGMPRIS("The MPRIS image is updated to the image from: %s",
544 NS_ConvertUTF16toUTF8(mCurrentImageUrl).get());
545 } else {
546 LOGMPRIS("Failed to set image to MPRIS");
547 mCurrentImageUrl.Truncate();
550 mFetchingUrl.Truncate();
552 [this, self](bool) {
553 LOGMPRIS("Failed to fetch image. Try next image");
554 mImageFetchRequest.Complete();
555 mFetchingUrl.Truncate();
556 LoadImageAtIndex(mNextImageIndex++);
558 ->Track(mImageFetchRequest);
561 bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData,
562 uint32_t aDataSize) {
563 if (!RenewLocalImageFile(aImageData, aDataSize)) {
564 return false;
566 MOZ_ASSERT(mLocalImageFile);
568 mMPRISMetadata.mArtUrl = nsCString("file://");
569 mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath());
571 LOGMPRIS("The image file is created at %s", mMPRISMetadata.mArtUrl.get());
572 return EmitMetadataChanged();
575 bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData,
576 uint32_t aDataSize) {
577 MOZ_ASSERT(aImageData);
578 MOZ_ASSERT(aDataSize != 0);
580 if (!InitLocalImageFile()) {
581 LOGMPRIS("Failed to create a new image");
582 return false;
585 MOZ_ASSERT(mLocalImageFile);
586 nsCOMPtr<nsIOutputStream> out;
587 NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile,
588 PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
589 uint32_t written;
590 nsresult rv = out->Write(aImageData, aDataSize, &written);
591 if (NS_FAILED(rv) || written != aDataSize) {
592 LOGMPRIS("Failed to write an image file");
593 RemoveAllLocalImages();
594 return false;
597 return true;
600 static const char* GetImageFileExtension(const char* aMimeType) {
601 MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0);
602 return "png";
605 bool MPRISServiceHandler::InitLocalImageFile() {
606 RemoveAllLocalImages();
608 if (!InitLocalImageFolder()) {
609 return false;
612 MOZ_ASSERT(mLocalImageFolder);
613 MOZ_ASSERT(!mLocalImageFile);
614 nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile));
615 if (NS_FAILED(rv)) {
616 LOGMPRIS("Failed to get the image folder");
617 return false;
620 auto cleanup =
621 MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] {
622 mLocalImageFile = nullptr;
625 // Create an unique file name to work around the file caching mechanism in the
626 // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's
627 // MPRIS, this pair will be cached. As long as the filename is same, even the
628 // file content specified by Y is changed to Z, the image will stay unchanged.
629 // The image shown in the Ubuntu's notification is still X instead of Z.
630 // Changing the filename constantly works around this problem
631 char filename[64];
632 SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++,
633 GetImageFileExtension(mMimeType.get()));
635 rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename));
636 if (NS_FAILED(rv)) {
637 LOGMPRIS("Failed to create an image filename");
638 return false;
641 rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
642 if (NS_FAILED(rv)) {
643 LOGMPRIS("Failed to create an image file");
644 return false;
647 cleanup.release();
648 return true;
651 bool MPRISServiceHandler::InitLocalImageFolder() {
652 if (mLocalImageFolder && LocalImageFolderExists()) {
653 return true;
656 nsresult rv = NS_ERROR_FAILURE;
657 if (IsRunningUnderFlatpak()) {
658 // The XDG_DATA_HOME points to the same location in the host and guest
659 // filesystem.
660 if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) {
661 rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true,
662 getter_AddRefs(mLocalImageFolder));
664 } else {
665 rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR,
666 getter_AddRefs(mLocalImageFolder));
669 if (NS_FAILED(rv) || !mLocalImageFolder) {
670 LOGMPRIS("Failed to get the image folder");
671 return false;
674 auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; });
676 rv = mLocalImageFolder->Append(u"firefox-mpris"_ns);
677 if (NS_FAILED(rv)) {
678 LOGMPRIS("Failed to name an image folder");
679 return false;
682 if (!LocalImageFolderExists()) {
683 rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700);
684 if (NS_FAILED(rv)) {
685 LOGMPRIS("Failed to create an image folder");
686 return false;
690 cleanup.release();
691 return true;
694 void MPRISServiceHandler::RemoveAllLocalImages() {
695 if (!mLocalImageFolder || !LocalImageFolderExists()) {
696 return;
699 nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true);
700 if (NS_FAILED(rv)) {
701 // It's ok to fail. The next removal is called when updating the
702 // media-session image, or closing the MPRIS.
703 LOGMPRIS("Failed to remove images");
706 LOGMPRIS("Abandon %s",
707 mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing");
708 mMPRISMetadata.mArtUrl.Truncate();
709 mLocalImageFile = nullptr;
710 mLocalImageFolder = nullptr;
713 bool MPRISServiceHandler::LocalImageFolderExists() {
714 MOZ_ASSERT(mLocalImageFolder);
716 bool exists;
717 nsresult rv = mLocalImageFolder->Exists(&exists);
718 return NS_SUCCEEDED(rv) && exists;
721 GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const {
722 GVariantBuilder builder;
723 g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
724 g_variant_builder_add(&builder, "{sv}", "mpris:trackid",
725 g_variant_new("o", DBUS_MPRIS_TRACK_PATH));
727 g_variant_builder_add(
728 &builder, "{sv}", "xesam:title",
729 g_variant_new_string(static_cast<const gchar*>(
730 NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get())));
732 g_variant_builder_add(
733 &builder, "{sv}", "xesam:album",
734 g_variant_new_string(static_cast<const gchar*>(
735 NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get())));
737 GVariantBuilder artistBuilder;
738 g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as"));
739 g_variant_builder_add(
740 &artistBuilder, "s",
741 static_cast<const gchar*>(
742 NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get()));
743 g_variant_builder_add(&builder, "{sv}", "xesam:artist",
744 g_variant_builder_end(&artistBuilder));
746 if (!mMPRISMetadata.mArtUrl.IsEmpty()) {
747 g_variant_builder_add(&builder, "{sv}", "mpris:artUrl",
748 g_variant_new_string(static_cast<const gchar*>(
749 mMPRISMetadata.mArtUrl.get())));
752 return g_variant_builder_end(&builder);
755 void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey) const {
756 for (const auto& listener : mListeners) {
757 listener->OnActionPerformed(dom::MediaControlAction(aKey));
761 struct InterfaceProperty {
762 const char* interface;
763 const char* property;
765 static const std::unordered_map<dom::MediaControlKey, InterfaceProperty>
766 gKeyProperty = {
767 {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}},
768 {dom::MediaControlKey::Nexttrack,
769 {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}},
770 {dom::MediaControlKey::Previoustrack,
771 {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}},
772 {dom::MediaControlKey::Play, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}},
773 {dom::MediaControlKey::Pause,
774 {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}};
776 void MPRISServiceHandler::SetSupportedMediaKeys(
777 const MediaKeysArray& aSupportedKeys) {
778 uint32_t supportedKeys = 0;
779 for (const dom::MediaControlKey& key : aSupportedKeys) {
780 supportedKeys |= GetMediaKeyMask(key);
783 if (mSupportedKeys == supportedKeys) {
784 LOGMPRIS("Supported keys stay the same");
785 return;
788 uint32_t oldSupportedKeys = mSupportedKeys;
789 mSupportedKeys = supportedKeys;
791 // Emit related property changes
792 for (auto it : gKeyProperty) {
793 bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first);
794 bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first);
795 if (keyWasSupported != keyIsSupported) {
796 LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface,
797 it.second.property, keyIsSupported ? "true" : "false");
798 EmitSupportedKeyChanged(it.first, keyIsSupported);
803 bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const {
804 return mSupportedKeys & GetMediaKeyMask(aKey);
807 bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey,
808 bool aSupported) const {
809 auto it = gKeyProperty.find(aKey);
810 if (it == gKeyProperty.end()) {
811 LOGMPRIS("No property for %s", ToMediaControlKeyStr(aKey));
812 return false;
815 GVariantBuilder builder;
816 g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
817 g_variant_builder_add(&builder, "{sv}",
818 static_cast<const gchar*>(it->second.property),
819 g_variant_new_boolean(aSupported));
821 GVariant* parameters = g_variant_new(
822 "(sa{sv}as)", static_cast<const gchar*>(it->second.interface), &builder,
823 nullptr);
825 LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface,
826 it->second.property);
827 return EmitPropertiesChangedSignal(parameters);
830 bool MPRISServiceHandler::EmitPropertiesChangedSignal(
831 GVariant* aParameters) const {
832 if (!mConnection) {
833 LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal");
834 return false;
837 GError* error = nullptr;
838 if (!g_dbus_connection_emit_signal(
839 mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH,
840 "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters,
841 &error)) {
842 LOGMPRIS("Failed to emit MPRIS property changes: %s",
843 error ? error->message : "Unknown Error");
844 if (error) {
845 g_error_free(error);
847 return false;
850 return true;
853 #undef LOGMPRIS
855 } // namespace widget
856 } // namespace mozilla