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