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