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"
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 "AsyncDBus.h"
30 #define LOGMPRIS(msg, ...) \
31 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
32 ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))
37 // A global counter tracking the total images saved in the system and it will be
38 // used to form a unique image file name.
39 static uint32_t gImageNumber
= 0;
41 static inline Maybe
<dom::MediaControlKey
> GetMediaControlKey(
42 const gchar
* aMethodName
) {
43 const std::unordered_map
<std::string
, dom::MediaControlKey
> map
= {
44 {"Raise", dom::MediaControlKey::Focus
},
45 {"Next", dom::MediaControlKey::Nexttrack
},
46 {"Previous", dom::MediaControlKey::Previoustrack
},
47 {"Pause", dom::MediaControlKey::Pause
},
48 {"PlayPause", dom::MediaControlKey::Playpause
},
49 {"Stop", dom::MediaControlKey::Stop
},
50 {"Play", dom::MediaControlKey::Play
}};
52 auto it
= map
.find(aMethodName
);
53 return it
== map
.end() ? Nothing() : Some(it
->second
);
56 static void HandleMethodCall(GDBusConnection
* aConnection
, const gchar
* aSender
,
57 const gchar
* aObjectPath
,
58 const gchar
* aInterfaceName
,
59 const gchar
* aMethodName
, GVariant
* aParameters
,
60 GDBusMethodInvocation
* aInvocation
,
62 MOZ_ASSERT(aUserData
);
63 MOZ_ASSERT(NS_IsMainThread());
65 Maybe
<dom::MediaControlKey
> key
= GetMediaControlKey(aMethodName
);
66 if (key
.isNothing()) {
67 g_dbus_method_invocation_return_error(
68 aInvocation
, G_DBUS_ERROR
, G_DBUS_ERROR_NOT_SUPPORTED
,
69 "Method %s.%s.%s not supported", aObjectPath
, aInterfaceName
,
74 MPRISServiceHandler
* handler
= static_cast<MPRISServiceHandler
*>(aUserData
);
75 if (handler
->PressKey(key
.value())) {
76 g_dbus_method_invocation_return_value(aInvocation
, nullptr);
78 g_dbus_method_invocation_return_error(
79 aInvocation
, G_DBUS_ERROR
, G_DBUS_ERROR_FAILED
,
80 "%s.%s.%s is not available now", aObjectPath
, aInterfaceName
,
85 enum class Property
: uint8_t {
103 static inline Maybe
<dom::MediaControlKey
> GetPairedKey(Property aProperty
) {
105 case Property::eCanRaise
:
106 return Some(dom::MediaControlKey::Focus
);
107 case Property::eCanGoNext
:
108 return Some(dom::MediaControlKey::Nexttrack
);
109 case Property::eCanGoPrevious
:
110 return Some(dom::MediaControlKey::Previoustrack
);
111 case Property::eCanPlay
:
112 return Some(dom::MediaControlKey::Play
);
113 case Property::eCanPause
:
114 return Some(dom::MediaControlKey::Pause
);
120 static inline Maybe
<Property
> GetProperty(const gchar
* aPropertyName
) {
121 const std::unordered_map
<std::string
, Property
> map
= {
122 // org.mpris.MediaPlayer2 properties
123 {"Identity", Property::eIdentity
},
124 {"DesktopEntry", Property::eDesktopEntry
},
125 {"HasTrackList", Property::eHasTrackList
},
126 {"CanRaise", Property::eCanRaise
},
127 {"CanQuit", Property::eCanQuit
},
128 {"SupportedUriSchemes", Property::eSupportedUriSchemes
},
129 {"SupportedMimeTypes", Property::eSupportedMimeTypes
},
130 // org.mpris.MediaPlayer2.Player properties
131 {"CanGoNext", Property::eCanGoNext
},
132 {"CanGoPrevious", Property::eCanGoPrevious
},
133 {"CanPlay", Property::eCanPlay
},
134 {"CanPause", Property::eCanPause
},
135 {"CanSeek", Property::eCanSeek
},
136 {"CanControl", Property::eCanControl
},
137 {"PlaybackStatus", Property::eGetPlaybackStatus
},
138 {"Metadata", Property::eGetMetadata
}};
140 auto it
= map
.find(aPropertyName
);
141 return (it
== map
.end() ? Nothing() : Some(it
->second
));
144 static GVariant
* HandleGetProperty(GDBusConnection
* aConnection
,
145 const gchar
* aSender
,
146 const gchar
* aObjectPath
,
147 const gchar
* aInterfaceName
,
148 const gchar
* aPropertyName
, GError
** aError
,
149 gpointer aUserData
) {
150 MOZ_ASSERT(aUserData
);
151 MOZ_ASSERT(NS_IsMainThread());
153 Maybe
<Property
> property
= GetProperty(aPropertyName
);
154 if (property
.isNothing()) {
155 g_set_error(aError
, G_DBUS_ERROR
, G_DBUS_ERROR_NOT_SUPPORTED
,
156 "%s.%s %s is not supported", aObjectPath
, aInterfaceName
,
161 MPRISServiceHandler
* handler
= static_cast<MPRISServiceHandler
*>(aUserData
);
162 switch (property
.value()) {
163 case Property::eSupportedUriSchemes
:
164 case Property::eSupportedMimeTypes
:
165 // No plan to implement OpenUri for now
166 return g_variant_new_strv(nullptr, 0);
167 case Property::eGetPlaybackStatus
:
168 return handler
->GetPlaybackStatus();
169 case Property::eGetMetadata
:
170 return handler
->GetMetadataAsGVariant();
171 case Property::eIdentity
:
172 return g_variant_new_string(handler
->Identity());
173 case Property::eDesktopEntry
:
174 return g_variant_new_string(handler
->DesktopEntry());
175 case Property::eHasTrackList
:
176 case Property::eCanQuit
:
177 case Property::eCanSeek
:
178 return g_variant_new_boolean(false);
179 // Play/Pause would be blocked if CanControl is false
180 case Property::eCanControl
:
181 return g_variant_new_boolean(true);
182 case Property::eCanRaise
:
183 case Property::eCanGoNext
:
184 case Property::eCanGoPrevious
:
185 case Property::eCanPlay
:
186 case Property::eCanPause
:
187 Maybe
<dom::MediaControlKey
> key
= GetPairedKey(property
.value());
188 MOZ_ASSERT(key
.isSome());
189 return g_variant_new_boolean(handler
->IsMediaKeySupported(key
.value()));
192 MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete");
196 static gboolean
HandleSetProperty(GDBusConnection
* aConnection
,
197 const gchar
* aSender
,
198 const gchar
* aObjectPath
,
199 const gchar
* aInterfaceName
,
200 const gchar
* aPropertyName
, GVariant
* aValue
,
201 GError
** aError
, gpointer aUserData
) {
202 MOZ_ASSERT(aUserData
);
203 MOZ_ASSERT(NS_IsMainThread());
204 g_set_error(aError
, G_IO_ERROR
, G_IO_ERROR_FAILED
,
205 "%s:%s setting is not supported", aInterfaceName
, aPropertyName
);
209 static const GDBusInterfaceVTable gInterfaceVTable
= {
210 HandleMethodCall
, HandleGetProperty
, HandleSetProperty
};
212 void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection
* aConnection
,
214 gpointer aUserData
) {
215 MOZ_ASSERT(aUserData
);
216 static_cast<MPRISServiceHandler
*>(aUserData
)->OnNameAcquired(aConnection
,
220 void MPRISServiceHandler::OnNameLostStatic(GDBusConnection
* aConnection
,
222 gpointer aUserData
) {
223 MOZ_ASSERT(aUserData
);
224 static_cast<MPRISServiceHandler
*>(aUserData
)->OnNameLost(aConnection
, aName
);
227 void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection
* aConnection
,
229 gpointer aUserData
) {
230 MOZ_ASSERT(aUserData
);
231 static_cast<MPRISServiceHandler
*>(aUserData
)->OnBusAcquired(aConnection
,
235 void MPRISServiceHandler::OnNameAcquired(GDBusConnection
* aConnection
,
236 const gchar
* aName
) {
237 LOGMPRIS("OnNameAcquired: %s", aName
);
238 mConnection
= aConnection
;
241 void MPRISServiceHandler::OnNameLost(GDBusConnection
* aConnection
,
242 const gchar
* aName
) {
243 LOGMPRIS("OnNameLost: %s", aName
);
244 mConnection
= nullptr;
245 if (!mRootRegistrationId
) {
253 if (g_dbus_connection_unregister_object(aConnection
, mRootRegistrationId
)) {
254 mRootRegistrationId
= 0;
256 // Note: Most code examples in the internet probably dont't even check the
258 // according to the spec it _can_ return false.
259 LOGMPRIS("Unable to unregister root object from within onNameLost!");
262 if (!mPlayerRegistrationId
) {
266 if (g_dbus_connection_unregister_object(aConnection
, mPlayerRegistrationId
)) {
267 mPlayerRegistrationId
= 0;
269 // Note: Most code examples in the internet probably dont't even check the
271 // according to the spec it _can_ return false.
272 LOGMPRIS("Unable to unregister object from within onNameLost!");
276 void MPRISServiceHandler::OnBusAcquired(GDBusConnection
* aConnection
,
277 const gchar
* aName
) {
278 GUniquePtr
<GError
> error
;
279 LOGMPRIS("OnBusAcquired: %s", aName
);
281 mRootRegistrationId
= g_dbus_connection_register_object(
282 aConnection
, DBUS_MPRIS_OBJECT_PATH
, mIntrospectionData
->interfaces
[0],
283 &gInterfaceVTable
, this, /* user_data */
284 nullptr, /* user_data_free_func */
285 getter_Transfers(error
)); /* GError** */
287 if (mRootRegistrationId
== 0) {
288 LOGMPRIS("Failed at root registration: %s",
289 error
? error
->message
: "Unknown Error");
293 mPlayerRegistrationId
= g_dbus_connection_register_object(
294 aConnection
, DBUS_MPRIS_OBJECT_PATH
, mIntrospectionData
->interfaces
[1],
295 &gInterfaceVTable
, this, /* user_data */
296 nullptr, /* user_data_free_func */
297 getter_Transfers(error
)); /* GError** */
299 if (mPlayerRegistrationId
== 0) {
300 LOGMPRIS("Failed at object registration: %s",
301 error
? error
->message
: "Unknown Error");
305 void MPRISServiceHandler::SetServiceName(const char* aName
) {
306 nsCString
dbusName(aName
);
307 dbusName
.ReplaceChar(':', '_');
308 dbusName
.ReplaceChar('.', '_');
310 nsCString(DBUS_MPRIS_SERVICE_NAME
) + nsCString(".instance") + dbusName
;
313 const char* MPRISServiceHandler::GetServiceName() { return mServiceName
.get(); }
316 void g_bus_get_callback(GObject
* aSourceObject
, GAsyncResult
* aRes
,
317 gpointer aUserData
) {
318 GUniquePtr
<GError
> error
;
320 GDBusConnection
* conn
= g_bus_get_finish(aRes
, getter_Transfers(error
));
322 if (!IsCancelledGError(error
.get())) {
323 NS_WARNING(nsPrintfCString("Failure at g_bus_get_finish: %s",
324 error
? error
->message
: "Unknown Error")
330 MPRISServiceHandler
* handler
= static_cast<MPRISServiceHandler
*>(aUserData
);
333 nsPrintfCString("Failure to get a MPRISServiceHandler*: %p", handler
)
338 handler
->OwnName(conn
);
341 void MPRISServiceHandler::OwnName(GDBusConnection
* aConnection
) {
342 MOZ_ASSERT(NS_IsMainThread());
344 SetServiceName(g_dbus_connection_get_unique_name(aConnection
));
346 GUniquePtr
<GError
> error
;
349 mOwnerId
= g_bus_own_name_on_connection(
350 aConnection
, GetServiceName(),
351 // Enter a waiting queue until this service name is free
352 // (likely another FF instance is running/has been crashed)
353 G_BUS_NAME_OWNER_FLAGS_NONE
, OnNameAcquiredStatic
, OnNameLostStatic
, this,
356 /* parse introspection data */
357 mIntrospectionData
= dont_AddRef(
358 g_dbus_node_info_new_for_xml(introspection_xml
, getter_Transfers(error
)));
360 if (!mIntrospectionData
) {
361 LOGMPRIS("Failed at parsing XML Interface definition: %s",
362 error
? error
->message
: "Unknown Error");
366 OnBusAcquired(aConnection
, GetServiceName());
369 bool MPRISServiceHandler::Open() {
370 MOZ_ASSERT(!mInitialized
);
371 MOZ_ASSERT(NS_IsMainThread());
373 mDBusGetCancellable
= dont_AddRef(g_cancellable_new());
374 g_bus_get(G_BUS_TYPE_SESSION
, mDBusGetCancellable
, g_bus_get_callback
, this);
380 MPRISServiceHandler::MPRISServiceHandler() = default;
381 MPRISServiceHandler::~MPRISServiceHandler() {
382 MOZ_ASSERT(!mInitialized
, "Close hasn't been called!");
385 void MPRISServiceHandler::Close() {
386 // Reset playback state and metadata before disconnect from dbus.
387 SetPlaybackState(dom::MediaSessionPlaybackState::None
);
390 OnNameLost(mConnection
, GetServiceName());
392 if (mDBusGetCancellable
) {
393 g_cancellable_cancel(mDBusGetCancellable
);
394 mDBusGetCancellable
= nullptr;
398 g_bus_unown_name(mOwnerId
);
401 mIntrospectionData
= nullptr;
403 mInitialized
= false;
404 MediaControlKeySource::Close();
407 bool MPRISServiceHandler::IsOpened() const { return mInitialized
; }
409 void MPRISServiceHandler::InitIdentity() {
411 nsCOMPtr
<nsIXULAppInfo
> appInfo
=
412 do_GetService("@mozilla.org/xre/app-info;1", &rv
);
414 MOZ_ASSERT(NS_SUCCEEDED(rv
));
415 rv
= appInfo
->GetVendor(mIdentity
);
416 MOZ_ASSERT(NS_SUCCEEDED(rv
));
417 rv
= appInfo
->GetName(mDesktopEntry
);
418 MOZ_ASSERT(NS_SUCCEEDED(rv
));
420 mIdentity
.Append(' ');
421 mIdentity
.Append(mDesktopEntry
);
423 // Compute the desktop entry name like nsAppRunner does for g_set_prgname
424 ToLowerCase(mDesktopEntry
);
427 const char* MPRISServiceHandler::Identity() const {
428 NS_WARNING_ASSERTION(mInitialized
,
429 "MPRISServiceHandler should have been initialized.");
430 return mIdentity
.get();
433 const char* MPRISServiceHandler::DesktopEntry() const {
434 NS_WARNING_ASSERTION(mInitialized
,
435 "MPRISServiceHandler should have been initialized.");
436 return mDesktopEntry
.get();
439 bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey
) const {
440 MOZ_ASSERT(mInitialized
);
441 if (!IsMediaKeySupported(aKey
)) {
442 LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey
));
445 LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey
));
450 void MPRISServiceHandler::SetPlaybackState(
451 dom::MediaSessionPlaybackState aState
) {
452 LOGMPRIS("SetPlaybackState");
453 if (mPlaybackState
== aState
) {
457 MediaControlKeySource::SetPlaybackState(aState
);
459 GVariant
* state
= GetPlaybackStatus();
460 GVariantBuilder builder
;
461 g_variant_builder_init(&builder
, G_VARIANT_TYPE("a{sv}"));
462 g_variant_builder_add(&builder
, "{sv}", "PlaybackStatus", state
);
464 GVariant
* parameters
= g_variant_new(
465 "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE
, &builder
, nullptr);
467 LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'");
468 Unused
<< EmitPropertiesChangedSignal(parameters
);
471 GVariant
* MPRISServiceHandler::GetPlaybackStatus() const {
472 switch (GetPlaybackState()) {
473 case dom::MediaSessionPlaybackState::Playing
:
474 return g_variant_new_string("Playing");
475 case dom::MediaSessionPlaybackState::Paused
:
476 return g_variant_new_string("Paused");
477 case dom::MediaSessionPlaybackState::None
:
478 return g_variant_new_string("Stopped");
480 MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
485 void MPRISServiceHandler::SetMediaMetadata(
486 const dom::MediaMetadataBase
& aMetadata
) {
487 // Reset the index of the next available image to be fetched in the artwork,
488 // before checking the fetching process should be started or not. The image
489 // fetching process could be skipped if the image being fetching currently is
490 // in the artwork. If the current image fetching fails, the next availabe
491 // candidate should be the first image in the latest artwork
494 // No need to fetch a MPRIS image if
495 // 1) MPRIS image is being fetched, and the one in fetching is in the artwork
496 // 2) MPRIS image is not being fetched, and the one in use is in the artwork
497 if (!mFetchingUrl
.IsEmpty()) {
498 if (dom::IsImageIn(aMetadata
.mArtwork
, mFetchingUrl
)) {
500 "No need to load MPRIS image. The one being processed is in the "
502 // Set MPRIS without the image first. The image will be loaded to MPRIS
503 // asynchronously once it's fetched and saved into a local file
504 SetMediaMetadataInternal(aMetadata
);
507 } else if (!mCurrentImageUrl
.IsEmpty()) {
508 if (dom::IsImageIn(aMetadata
.mArtwork
, mCurrentImageUrl
)) {
509 LOGMPRIS("No need to load MPRIS image. The one in use is in the artwork");
510 SetMediaMetadataInternal(aMetadata
, false);
515 // Set MPRIS without the image first then load the image to MPRIS
517 SetMediaMetadataInternal(aMetadata
);
518 LoadImageAtIndex(mNextImageIndex
++);
521 bool MPRISServiceHandler::EmitMetadataChanged() const {
522 GVariantBuilder builder
;
523 g_variant_builder_init(&builder
, G_VARIANT_TYPE("a{sv}"));
524 g_variant_builder_add(&builder
, "{sv}", "Metadata", GetMetadataAsGVariant());
526 GVariant
* parameters
= g_variant_new(
527 "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE
, &builder
, nullptr);
529 LOGMPRIS("Emit MPRIS property changes for 'Metadata'");
530 return EmitPropertiesChangedSignal(parameters
);
533 void MPRISServiceHandler::SetMediaMetadataInternal(
534 const dom::MediaMetadataBase
& aMetadata
, bool aClearArtUrl
) {
535 mMPRISMetadata
.UpdateFromMetadataBase(aMetadata
);
537 mMPRISMetadata
.mArtUrl
.Truncate();
539 EmitMetadataChanged();
542 void MPRISServiceHandler::ClearMetadata() {
543 mMPRISMetadata
.Clear();
544 mImageFetchRequest
.DisconnectIfExists();
545 RemoveAllLocalImages();
546 mCurrentImageUrl
.Truncate();
547 mFetchingUrl
.Truncate();
550 EmitMetadataChanged();
553 void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex
) {
554 MOZ_ASSERT(NS_IsMainThread());
556 if (aIndex
>= mMPRISMetadata
.mArtwork
.Length()) {
557 LOGMPRIS("Stop loading image to MPRIS. No available image");
558 mImageFetchRequest
.DisconnectIfExists();
562 const dom::MediaImage
& image
= mMPRISMetadata
.mArtwork
[aIndex
];
564 if (!dom::IsValidImageUrl(image
.mSrc
)) {
565 LOGMPRIS("Skip the image with invalid URL. Try next image");
566 LoadImageAtIndex(mNextImageIndex
++);
570 mImageFetchRequest
.DisconnectIfExists();
571 mFetchingUrl
= image
.mSrc
;
573 mImageFetcher
= MakeUnique
<dom::FetchImageHelper
>(image
);
574 RefPtr
<MPRISServiceHandler
> self
= this;
575 mImageFetcher
->FetchImage()
577 AbstractThread::MainThread(), __func__
,
578 [this, self
](const nsCOMPtr
<imgIContainer
>& aImage
) {
579 LOGMPRIS("The image is fetched successfully");
580 mImageFetchRequest
.Complete();
583 char* data
= nullptr;
584 // Only used to hold the image data
585 nsCOMPtr
<nsIInputStream
> inputStream
;
586 nsresult rv
= dom::GetEncodedImageBuffer(
587 aImage
, mMimeType
, getter_AddRefs(inputStream
), &size
, &data
);
588 if (NS_FAILED(rv
) || !inputStream
|| size
== 0 || !data
) {
589 LOGMPRIS("Failed to get the image buffer info. Try next image");
590 LoadImageAtIndex(mNextImageIndex
++);
594 if (SetImageToDisplay(data
, size
)) {
595 mCurrentImageUrl
= mFetchingUrl
;
596 LOGMPRIS("The MPRIS image is updated to the image from: %s",
597 NS_ConvertUTF16toUTF8(mCurrentImageUrl
).get());
599 LOGMPRIS("Failed to set image to MPRIS");
600 mCurrentImageUrl
.Truncate();
603 mFetchingUrl
.Truncate();
606 LOGMPRIS("Failed to fetch image. Try next image");
607 mImageFetchRequest
.Complete();
608 mFetchingUrl
.Truncate();
609 LoadImageAtIndex(mNextImageIndex
++);
611 ->Track(mImageFetchRequest
);
614 bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData
,
615 uint32_t aDataSize
) {
616 if (!RenewLocalImageFile(aImageData
, aDataSize
)) {
619 MOZ_ASSERT(mLocalImageFile
);
621 mMPRISMetadata
.mArtUrl
= nsCString("file://");
622 mMPRISMetadata
.mArtUrl
.Append(mLocalImageFile
->NativePath());
624 LOGMPRIS("The image file is created at %s", mMPRISMetadata
.mArtUrl
.get());
625 return EmitMetadataChanged();
628 bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData
,
629 uint32_t aDataSize
) {
630 MOZ_ASSERT(aImageData
);
631 MOZ_ASSERT(aDataSize
!= 0);
633 if (!InitLocalImageFile()) {
634 LOGMPRIS("Failed to create a new image");
638 MOZ_ASSERT(mLocalImageFile
);
639 nsCOMPtr
<nsIOutputStream
> out
;
640 NS_NewLocalFileOutputStream(getter_AddRefs(out
), mLocalImageFile
,
641 PR_RDWR
| PR_CREATE_FILE
| PR_TRUNCATE
);
643 nsresult rv
= out
->Write(aImageData
, aDataSize
, &written
);
644 if (NS_FAILED(rv
) || written
!= aDataSize
) {
645 LOGMPRIS("Failed to write an image file");
646 RemoveAllLocalImages();
653 static const char* GetImageFileExtension(const char* aMimeType
) {
654 MOZ_ASSERT(strcmp(aMimeType
, IMAGE_PNG
) == 0);
658 bool MPRISServiceHandler::InitLocalImageFile() {
659 RemoveAllLocalImages();
661 if (!InitLocalImageFolder()) {
665 MOZ_ASSERT(mLocalImageFolder
);
666 MOZ_ASSERT(!mLocalImageFile
);
667 nsresult rv
= mLocalImageFolder
->Clone(getter_AddRefs(mLocalImageFile
));
669 LOGMPRIS("Failed to get the image folder");
674 MakeScopeExit([this, self
= RefPtr
<MPRISServiceHandler
>(this)] {
675 mLocalImageFile
= nullptr;
678 // Create an unique file name to work around the file caching mechanism in the
679 // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's
680 // MPRIS, this pair will be cached. As long as the filename is same, even the
681 // file content specified by Y is changed to Z, the image will stay unchanged.
682 // The image shown in the Ubuntu's notification is still X instead of Z.
683 // Changing the filename constantly works around this problem
685 SprintfLiteral(filename
, "%d_%d.%s", getpid(), gImageNumber
++,
686 GetImageFileExtension(mMimeType
.get()));
688 rv
= mLocalImageFile
->Append(NS_ConvertUTF8toUTF16(filename
));
690 LOGMPRIS("Failed to create an image filename");
694 rv
= mLocalImageFile
->Create(nsIFile::NORMAL_FILE_TYPE
, 0600);
696 LOGMPRIS("Failed to create an image file");
704 bool MPRISServiceHandler::InitLocalImageFolder() {
705 if (mLocalImageFolder
&& LocalImageFolderExists()) {
709 nsresult rv
= NS_ERROR_FAILURE
;
710 if (IsRunningUnderFlatpak()) {
711 // The XDG_DATA_HOME points to the same location in the host and guest
713 if (const auto* xdgDataHome
= g_getenv("XDG_DATA_HOME")) {
714 rv
= NS_NewNativeLocalFile(nsDependentCString(xdgDataHome
), true,
715 getter_AddRefs(mLocalImageFolder
));
718 rv
= NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR
,
719 getter_AddRefs(mLocalImageFolder
));
722 if (NS_FAILED(rv
) || !mLocalImageFolder
) {
723 LOGMPRIS("Failed to get the image folder");
727 auto cleanup
= MakeScopeExit([&] { mLocalImageFolder
= nullptr; });
729 rv
= mLocalImageFolder
->Append(u
"firefox-mpris"_ns
);
731 LOGMPRIS("Failed to name an image folder");
735 if (!LocalImageFolderExists()) {
736 rv
= mLocalImageFolder
->Create(nsIFile::DIRECTORY_TYPE
, 0700);
738 LOGMPRIS("Failed to create an image folder");
747 void MPRISServiceHandler::RemoveAllLocalImages() {
748 if (!mLocalImageFolder
|| !LocalImageFolderExists()) {
752 nsresult rv
= mLocalImageFolder
->Remove(/* aRecursive */ true);
754 // It's ok to fail. The next removal is called when updating the
755 // media-session image, or closing the MPRIS.
756 LOGMPRIS("Failed to remove images");
759 LOGMPRIS("Abandon %s",
760 mLocalImageFile
? mLocalImageFile
->NativePath().get() : "nothing");
761 mMPRISMetadata
.mArtUrl
.Truncate();
762 mLocalImageFile
= nullptr;
763 mLocalImageFolder
= nullptr;
766 bool MPRISServiceHandler::LocalImageFolderExists() {
767 MOZ_ASSERT(mLocalImageFolder
);
770 nsresult rv
= mLocalImageFolder
->Exists(&exists
);
771 return NS_SUCCEEDED(rv
) && exists
;
774 GVariant
* MPRISServiceHandler::GetMetadataAsGVariant() const {
775 GVariantBuilder builder
;
776 g_variant_builder_init(&builder
, G_VARIANT_TYPE("a{sv}"));
777 g_variant_builder_add(&builder
, "{sv}", "mpris:trackid",
778 g_variant_new("o", DBUS_MPRIS_TRACK_PATH
));
780 g_variant_builder_add(
781 &builder
, "{sv}", "xesam:title",
782 g_variant_new_string(static_cast<const gchar
*>(
783 NS_ConvertUTF16toUTF8(mMPRISMetadata
.mTitle
).get())));
785 g_variant_builder_add(
786 &builder
, "{sv}", "xesam:album",
787 g_variant_new_string(static_cast<const gchar
*>(
788 NS_ConvertUTF16toUTF8(mMPRISMetadata
.mAlbum
).get())));
790 GVariantBuilder artistBuilder
;
791 g_variant_builder_init(&artistBuilder
, G_VARIANT_TYPE("as"));
792 g_variant_builder_add(
794 static_cast<const gchar
*>(
795 NS_ConvertUTF16toUTF8(mMPRISMetadata
.mArtist
).get()));
796 g_variant_builder_add(&builder
, "{sv}", "xesam:artist",
797 g_variant_builder_end(&artistBuilder
));
799 if (!mMPRISMetadata
.mArtUrl
.IsEmpty()) {
800 g_variant_builder_add(&builder
, "{sv}", "mpris:artUrl",
801 g_variant_new_string(static_cast<const gchar
*>(
802 mMPRISMetadata
.mArtUrl
.get())));
805 return g_variant_builder_end(&builder
);
808 void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey
) const {
809 for (const auto& listener
: mListeners
) {
810 listener
->OnActionPerformed(dom::MediaControlAction(aKey
));
814 struct InterfaceProperty
{
815 const char* interface
;
816 const char* property
;
818 static const std::unordered_map
<dom::MediaControlKey
, InterfaceProperty
>
820 {dom::MediaControlKey::Focus
, {DBUS_MPRIS_INTERFACE
, "CanRaise"}},
821 {dom::MediaControlKey::Nexttrack
,
822 {DBUS_MPRIS_PLAYER_INTERFACE
, "CanGoNext"}},
823 {dom::MediaControlKey::Previoustrack
,
824 {DBUS_MPRIS_PLAYER_INTERFACE
, "CanGoPrevious"}},
825 {dom::MediaControlKey::Play
, {DBUS_MPRIS_PLAYER_INTERFACE
, "CanPlay"}},
826 {dom::MediaControlKey::Pause
,
827 {DBUS_MPRIS_PLAYER_INTERFACE
, "CanPause"}}};
829 void MPRISServiceHandler::SetSupportedMediaKeys(
830 const MediaKeysArray
& aSupportedKeys
) {
831 uint32_t supportedKeys
= 0;
832 for (const dom::MediaControlKey
& key
: aSupportedKeys
) {
833 supportedKeys
|= GetMediaKeyMask(key
);
836 if (mSupportedKeys
== supportedKeys
) {
837 LOGMPRIS("Supported keys stay the same");
841 uint32_t oldSupportedKeys
= mSupportedKeys
;
842 mSupportedKeys
= supportedKeys
;
844 // Emit related property changes
845 for (auto it
: gKeyProperty
) {
846 bool keyWasSupported
= oldSupportedKeys
& GetMediaKeyMask(it
.first
);
847 bool keyIsSupported
= mSupportedKeys
& GetMediaKeyMask(it
.first
);
848 if (keyWasSupported
!= keyIsSupported
) {
849 LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it
.second
.interface
,
850 it
.second
.property
, keyIsSupported
? "true" : "false");
851 EmitSupportedKeyChanged(it
.first
, keyIsSupported
);
856 bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey
) const {
857 return mSupportedKeys
& GetMediaKeyMask(aKey
);
860 bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey
,
861 bool aSupported
) const {
862 auto it
= gKeyProperty
.find(aKey
);
863 if (it
== gKeyProperty
.end()) {
864 LOGMPRIS("No property for %s", ToMediaControlKeyStr(aKey
));
868 GVariantBuilder builder
;
869 g_variant_builder_init(&builder
, G_VARIANT_TYPE("a{sv}"));
870 g_variant_builder_add(&builder
, "{sv}",
871 static_cast<const gchar
*>(it
->second
.property
),
872 g_variant_new_boolean(aSupported
));
874 GVariant
* parameters
= g_variant_new(
875 "(sa{sv}as)", static_cast<const gchar
*>(it
->second
.interface
), &builder
,
878 LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it
->second
.interface
,
879 it
->second
.property
);
880 return EmitPropertiesChangedSignal(parameters
);
883 bool MPRISServiceHandler::EmitPropertiesChangedSignal(
884 GVariant
* aParameters
) const {
886 LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal");
890 GError
* error
= nullptr;
891 if (!g_dbus_connection_emit_signal(
892 mConnection
, nullptr, DBUS_MPRIS_OBJECT_PATH
,
893 "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters
,
895 LOGMPRIS("Failed to emit MPRIS property changes: %s",
896 error
? error
->message
: "Unknown Error");
908 } // namespace widget
909 } // namespace mozilla