Backed out 3 changesets (bug 1870106, bug 1845276) for causing doc generate failures...
[gecko.git] / widget / gtk / MPRISServiceHandler.cpp
blobae1ef8654d2e38faed312203157fc6642191165c
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 "AsyncDBus.h"
28 #include "prio.h"
30 #define LOGMPRIS(msg, ...) \
31 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
32 ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))
34 namespace mozilla {
35 namespace widget {
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,
61 gpointer aUserData) {
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,
70 aMethodName);
71 return;
74 MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
75 if (handler->PressKey(key.value())) {
76 g_dbus_method_invocation_return_value(aInvocation, nullptr);
77 } else {
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,
81 aMethodName);
85 enum class Property : uint8_t {
86 eIdentity,
87 eDesktopEntry,
88 eHasTrackList,
89 eCanRaise,
90 eCanQuit,
91 eSupportedUriSchemes,
92 eSupportedMimeTypes,
93 eCanGoNext,
94 eCanGoPrevious,
95 eCanPlay,
96 eCanPause,
97 eCanSeek,
98 eCanControl,
99 eGetPlaybackStatus,
100 eGetMetadata,
103 static inline Maybe<dom::MediaControlKey> GetPairedKey(Property aProperty) {
104 switch (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);
115 default:
116 return Nothing();
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,
157 aPropertyName);
158 return nullptr;
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");
193 return nullptr;
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);
206 return false;
209 static const GDBusInterfaceVTable gInterfaceVTable = {
210 HandleMethodCall, HandleGetProperty, HandleSetProperty};
212 void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection,
213 const gchar* aName,
214 gpointer aUserData) {
215 MOZ_ASSERT(aUserData);
216 static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection,
217 aName);
220 void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection,
221 const gchar* aName,
222 gpointer aUserData) {
223 MOZ_ASSERT(aUserData);
224 static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName);
227 void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection,
228 const gchar* aName,
229 gpointer aUserData) {
230 MOZ_ASSERT(aUserData);
231 static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection,
232 aName);
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) {
246 return;
249 if (!aConnection) {
250 return;
253 if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) {
254 mRootRegistrationId = 0;
255 } else {
256 // Note: Most code examples in the internet probably dont't even check the
257 // result here, but
258 // according to the spec it _can_ return false.
259 LOGMPRIS("Unable to unregister root object from within onNameLost!");
262 if (!mPlayerRegistrationId) {
263 return;
266 if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) {
267 mPlayerRegistrationId = 0;
268 } else {
269 // Note: Most code examples in the internet probably dont't even check the
270 // result here, but
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");
290 return;
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('.', '_');
309 mServiceName =
310 nsCString(DBUS_MPRIS_SERVICE_NAME) + nsCString(".instance") + dbusName;
313 const char* MPRISServiceHandler::GetServiceName() { return mServiceName.get(); }
315 /* static */
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));
321 if (!conn) {
322 if (!IsCancelledGError(error.get())) {
323 NS_WARNING(nsPrintfCString("Failure at g_bus_get_finish: %s",
324 error ? error->message : "Unknown Error")
325 .get());
327 return;
330 MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
331 if (!handler) {
332 NS_WARNING(
333 nsPrintfCString("Failure to get a MPRISServiceHandler*: %p", handler)
334 .get());
335 return;
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;
348 InitIdentity();
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,
354 nullptr);
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");
363 return;
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);
376 mInitialized = true;
377 return true;
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);
388 ClearMetadata();
390 OnNameLost(mConnection, GetServiceName());
392 if (mDBusGetCancellable) {
393 g_cancellable_cancel(mDBusGetCancellable);
394 mDBusGetCancellable = nullptr;
397 if (mOwnerId != 0) {
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() {
410 nsresult rv;
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));
443 return false;
445 LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey));
446 EmitEvent(aKey);
447 return true;
450 void MPRISServiceHandler::SetPlaybackState(
451 dom::MediaSessionPlaybackState aState) {
452 LOGMPRIS("SetPlaybackState");
453 if (mPlaybackState == aState) {
454 return;
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");
479 default:
480 MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
481 return nullptr;
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
492 mNextImageIndex = 0;
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)) {
499 LOGMPRIS(
500 "No need to load MPRIS image. The one being processed is in the "
501 "artwork");
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);
505 return;
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);
511 return;
515 // Set MPRIS without the image first then load the image to MPRIS
516 // asynchronously
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);
536 if (aClearArtUrl) {
537 mMPRISMetadata.mArtUrl.Truncate();
539 EmitMetadataChanged();
542 void MPRISServiceHandler::ClearMetadata() {
543 mMPRISMetadata.Clear();
544 mImageFetchRequest.DisconnectIfExists();
545 RemoveAllLocalImages();
546 mCurrentImageUrl.Truncate();
547 mFetchingUrl.Truncate();
548 mNextImageIndex = 0;
549 mSupportedKeys = 0;
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();
559 return;
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++);
567 return;
570 mImageFetchRequest.DisconnectIfExists();
571 mFetchingUrl = image.mSrc;
573 mImageFetcher = MakeUnique<dom::FetchImageHelper>(image);
574 RefPtr<MPRISServiceHandler> self = this;
575 mImageFetcher->FetchImage()
576 ->Then(
577 AbstractThread::MainThread(), __func__,
578 [this, self](const nsCOMPtr<imgIContainer>& aImage) {
579 LOGMPRIS("The image is fetched successfully");
580 mImageFetchRequest.Complete();
582 uint32_t size = 0;
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++);
591 return;
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());
598 } else {
599 LOGMPRIS("Failed to set image to MPRIS");
600 mCurrentImageUrl.Truncate();
603 mFetchingUrl.Truncate();
605 [this, self](bool) {
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)) {
617 return false;
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");
635 return false;
638 MOZ_ASSERT(mLocalImageFile);
639 nsCOMPtr<nsIOutputStream> out;
640 NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile,
641 PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
642 uint32_t written;
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();
647 return false;
650 return true;
653 static const char* GetImageFileExtension(const char* aMimeType) {
654 MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0);
655 return "png";
658 bool MPRISServiceHandler::InitLocalImageFile() {
659 RemoveAllLocalImages();
661 if (!InitLocalImageFolder()) {
662 return false;
665 MOZ_ASSERT(mLocalImageFolder);
666 MOZ_ASSERT(!mLocalImageFile);
667 nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile));
668 if (NS_FAILED(rv)) {
669 LOGMPRIS("Failed to get the image folder");
670 return false;
673 auto cleanup =
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
684 char filename[64];
685 SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++,
686 GetImageFileExtension(mMimeType.get()));
688 rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename));
689 if (NS_FAILED(rv)) {
690 LOGMPRIS("Failed to create an image filename");
691 return false;
694 rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
695 if (NS_FAILED(rv)) {
696 LOGMPRIS("Failed to create an image file");
697 return false;
700 cleanup.release();
701 return true;
704 bool MPRISServiceHandler::InitLocalImageFolder() {
705 if (mLocalImageFolder && LocalImageFolderExists()) {
706 return true;
709 nsresult rv = NS_ERROR_FAILURE;
710 if (IsRunningUnderFlatpak()) {
711 // The XDG_DATA_HOME points to the same location in the host and guest
712 // filesystem.
713 if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) {
714 rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true,
715 getter_AddRefs(mLocalImageFolder));
717 } else {
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");
724 return false;
727 auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; });
729 rv = mLocalImageFolder->Append(u"firefox-mpris"_ns);
730 if (NS_FAILED(rv)) {
731 LOGMPRIS("Failed to name an image folder");
732 return false;
735 if (!LocalImageFolderExists()) {
736 rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700);
737 if (NS_FAILED(rv)) {
738 LOGMPRIS("Failed to create an image folder");
739 return false;
743 cleanup.release();
744 return true;
747 void MPRISServiceHandler::RemoveAllLocalImages() {
748 if (!mLocalImageFolder || !LocalImageFolderExists()) {
749 return;
752 nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true);
753 if (NS_FAILED(rv)) {
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);
769 bool exists;
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(
793 &artistBuilder, "s",
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>
819 gKeyProperty = {
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");
838 return;
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));
865 return false;
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,
876 nullptr);
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 {
885 if (!mConnection) {
886 LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal");
887 return false;
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,
894 &error)) {
895 LOGMPRIS("Failed to emit MPRIS property changes: %s",
896 error ? error->message : "Unknown Error");
897 if (error) {
898 g_error_free(error);
900 return false;
903 return true;
906 #undef LOGMPRIS
908 } // namespace widget
909 } // namespace mozilla