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