1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /* vim:expandtab:shiftwidth=4:tabstop=4:
3  */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5  * License, v. 2.0. If a copy of the MPL was not distributed with this
6  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 
8 #include <string.h>
9 
10 #include "nscore.h"
11 #include "plstr.h"
12 #include "prlink.h"
13 
14 #include "nsSound.h"
15 
16 #include "HeadlessSound.h"
17 #include "nsIURL.h"
18 #include "nsIFileURL.h"
19 #include "nsNetUtil.h"
20 #include "nsIChannel.h"
21 #include "nsCOMPtr.h"
22 #include "nsString.h"
23 #include "nsDirectoryService.h"
24 #include "nsDirectoryServiceDefs.h"
25 #include "mozilla/FileUtils.h"
26 #include "mozilla/Services.h"
27 #include "mozilla/Unused.h"
28 #include "mozilla/WidgetUtils.h"
29 #include "nsIXULAppInfo.h"
30 #include "nsContentUtils.h"
31 #include "gfxPlatform.h"
32 #include "mozilla/ClearOnShutdown.h"
33 
34 #include <stdio.h>
35 #include <unistd.h>
36 
37 #include <gtk/gtk.h>
38 static PRLibrary *libcanberra = nullptr;
39 
40 /* used to play sounds with libcanberra. */
41 typedef struct _ca_context ca_context;
42 typedef struct _ca_proplist ca_proplist;
43 
44 typedef void (*ca_finish_callback_t)(ca_context *c, uint32_t id, int error_code,
45                                      void *userdata);
46 
47 typedef int (*ca_context_create_fn)(ca_context **);
48 typedef int (*ca_context_destroy_fn)(ca_context *);
49 typedef int (*ca_context_play_fn)(ca_context *c, uint32_t id, ...);
50 typedef int (*ca_context_change_props_fn)(ca_context *c, ...);
51 typedef int (*ca_proplist_create_fn)(ca_proplist **);
52 typedef int (*ca_proplist_destroy_fn)(ca_proplist *);
53 typedef int (*ca_proplist_sets_fn)(ca_proplist *c, const char *key,
54                                    const char *value);
55 typedef int (*ca_context_play_full_fn)(ca_context *c, uint32_t id,
56                                        ca_proplist *p, ca_finish_callback_t cb,
57                                        void *userdata);
58 
59 static ca_context_create_fn ca_context_create;
60 static ca_context_destroy_fn ca_context_destroy;
61 static ca_context_play_fn ca_context_play;
62 static ca_context_change_props_fn ca_context_change_props;
63 static ca_proplist_create_fn ca_proplist_create;
64 static ca_proplist_destroy_fn ca_proplist_destroy;
65 static ca_proplist_sets_fn ca_proplist_sets;
66 static ca_context_play_full_fn ca_context_play_full;
67 
68 struct ScopedCanberraFile {
ScopedCanberraFileScopedCanberraFile69   explicit ScopedCanberraFile(nsIFile *file) : mFile(file){};
70 
~ScopedCanberraFileScopedCanberraFile71   ~ScopedCanberraFile() {
72     if (mFile) {
73       mFile->Remove(false);
74     }
75   }
76 
forgetScopedCanberraFile77   void forget() { mozilla::Unused << mFile.forget(); }
operator ->ScopedCanberraFile78   nsIFile *operator->() { return mFile; }
operator nsIFile*ScopedCanberraFile79   operator nsIFile *() { return mFile; }
80 
81   nsCOMPtr<nsIFile> mFile;
82 };
83 
ca_context_get_default()84 static ca_context *ca_context_get_default() {
85   // This allows us to avoid race conditions with freeing the context by handing
86   // that responsibility to Glib, and still use one context at a time
87   static GStaticPrivate ctx_static_private = G_STATIC_PRIVATE_INIT;
88 
89   ca_context *ctx = (ca_context *)g_static_private_get(&ctx_static_private);
90 
91   if (ctx) {
92     return ctx;
93   }
94 
95   ca_context_create(&ctx);
96   if (!ctx) {
97     return nullptr;
98   }
99 
100   g_static_private_set(&ctx_static_private, ctx,
101                        (GDestroyNotify)ca_context_destroy);
102 
103   GtkSettings *settings = gtk_settings_get_default();
104   if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
105                                    "gtk-sound-theme-name")) {
106     gchar *sound_theme_name = nullptr;
107     g_object_get(settings, "gtk-sound-theme-name", &sound_theme_name, nullptr);
108 
109     if (sound_theme_name) {
110       ca_context_change_props(ctx, "canberra.xdg-theme.name", sound_theme_name,
111                               nullptr);
112       g_free(sound_theme_name);
113     }
114   }
115 
116   nsAutoString wbrand;
117   mozilla::widget::WidgetUtils::GetBrandShortName(wbrand);
118   ca_context_change_props(ctx, "application.name",
119                           NS_ConvertUTF16toUTF8(wbrand).get(), nullptr);
120 
121   nsCOMPtr<nsIXULAppInfo> appInfo =
122       do_GetService("@mozilla.org/xre/app-info;1");
123   if (appInfo) {
124     nsAutoCString version;
125     appInfo->GetVersion(version);
126 
127     ca_context_change_props(ctx, "application.version", version.get(), nullptr);
128   }
129 
130   ca_context_change_props(ctx, "application.icon_name", MOZ_APP_NAME, nullptr);
131 
132   return ctx;
133 }
134 
ca_finish_cb(ca_context * c,uint32_t id,int error_code,void * userdata)135 static void ca_finish_cb(ca_context *c, uint32_t id, int error_code,
136                          void *userdata) {
137   nsIFile *file = reinterpret_cast<nsIFile *>(userdata);
138   if (file) {
139     file->Remove(false);
140     NS_RELEASE(file);
141   }
142 }
143 
NS_IMPL_ISUPPORTS(nsSound,nsISound,nsIStreamLoaderObserver)144 NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver)
145 
146 ////////////////////////////////////////////////////////////////////////
147 nsSound::nsSound() { mInited = false; }
148 
~nsSound()149 nsSound::~nsSound() {}
150 
151 NS_IMETHODIMP
Init()152 nsSound::Init() {
153   // This function is designed so that no library is compulsory, and
154   // one library missing doesn't cause the other(s) to not be used.
155   if (mInited) return NS_OK;
156 
157   mInited = true;
158 
159   if (!libcanberra) {
160     libcanberra = PR_LoadLibrary("libcanberra.so.0");
161     if (libcanberra) {
162       ca_context_create = (ca_context_create_fn)PR_FindFunctionSymbol(
163           libcanberra, "ca_context_create");
164       if (!ca_context_create) {
165         PR_UnloadLibrary(libcanberra);
166         libcanberra = nullptr;
167       } else {
168         // at this point we know we have a good libcanberra library
169         ca_context_destroy = (ca_context_destroy_fn)PR_FindFunctionSymbol(
170             libcanberra, "ca_context_destroy");
171         ca_context_play = (ca_context_play_fn)PR_FindFunctionSymbol(
172             libcanberra, "ca_context_play");
173         ca_context_change_props =
174             (ca_context_change_props_fn)PR_FindFunctionSymbol(
175                 libcanberra, "ca_context_change_props");
176         ca_proplist_create = (ca_proplist_create_fn)PR_FindFunctionSymbol(
177             libcanberra, "ca_proplist_create");
178         ca_proplist_destroy = (ca_proplist_destroy_fn)PR_FindFunctionSymbol(
179             libcanberra, "ca_proplist_destroy");
180         ca_proplist_sets = (ca_proplist_sets_fn)PR_FindFunctionSymbol(
181             libcanberra, "ca_proplist_sets");
182         ca_context_play_full = (ca_context_play_full_fn)PR_FindFunctionSymbol(
183             libcanberra, "ca_context_play_full");
184       }
185     }
186   }
187 
188   return NS_OK;
189 }
190 
Shutdown()191 /* static */ void nsSound::Shutdown() {
192   if (libcanberra) {
193     PR_UnloadLibrary(libcanberra);
194     libcanberra = nullptr;
195   }
196 }
197 
198 namespace mozilla {
199 namespace sound {
200 StaticRefPtr<nsISound> sInstance;
201 }
202 }  // namespace mozilla
GetInstance()203 /* static */ already_AddRefed<nsISound> nsSound::GetInstance() {
204   using namespace mozilla::sound;
205 
206   if (!sInstance) {
207     if (gfxPlatform::IsHeadless()) {
208       sInstance = new mozilla::widget::HeadlessSound();
209     } else {
210       sInstance = new nsSound();
211     }
212     ClearOnShutdown(&sInstance);
213   }
214 
215   RefPtr<nsISound> service = sInstance.get();
216   return service.forget();
217 }
218 
OnStreamComplete(nsIStreamLoader * aLoader,nsISupports * context,nsresult aStatus,uint32_t dataLen,const uint8_t * data)219 NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader *aLoader,
220                                         nsISupports *context, nsresult aStatus,
221                                         uint32_t dataLen, const uint8_t *data) {
222   // print a load error on bad status, and return
223   if (NS_FAILED(aStatus)) {
224 #ifdef DEBUG
225     if (aLoader) {
226       nsCOMPtr<nsIRequest> request;
227       aLoader->GetRequest(getter_AddRefs(request));
228       if (request) {
229         nsCOMPtr<nsIURI> uri;
230         nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
231         if (channel) {
232           channel->GetURI(getter_AddRefs(uri));
233           if (uri) {
234             printf("Failed to load %s\n", uri->GetSpecOrDefault().get());
235           }
236         }
237       }
238     }
239 #endif
240     return aStatus;
241   }
242 
243   nsCOMPtr<nsIFile> tmpFile;
244   nsDirectoryService::gService->Get(NS_OS_TEMP_DIR, NS_GET_IID(nsIFile),
245                                     getter_AddRefs(tmpFile));
246 
247   nsresult rv =
248       tmpFile->AppendNative(nsDependentCString("mozilla_audio_sample"));
249   if (NS_FAILED(rv)) {
250     return rv;
251   }
252 
253   rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, PR_IRUSR | PR_IWUSR);
254   if (NS_FAILED(rv)) {
255     return rv;
256   }
257 
258   ScopedCanberraFile canberraFile(tmpFile);
259 
260   mozilla::AutoFDClose fd;
261   rv = canberraFile->OpenNSPRFileDesc(PR_WRONLY, PR_IRUSR | PR_IWUSR,
262                                       &fd.rwget());
263   if (NS_FAILED(rv)) {
264     return rv;
265   }
266 
267   // XXX: Should we do this on another thread?
268   uint32_t length = dataLen;
269   while (length > 0) {
270     int32_t amount = PR_Write(fd, data, length);
271     if (amount < 0) {
272       return NS_ERROR_FAILURE;
273     }
274     length -= amount;
275     data += amount;
276   }
277 
278   ca_context *ctx = ca_context_get_default();
279   if (!ctx) {
280     return NS_ERROR_OUT_OF_MEMORY;
281   }
282 
283   ca_proplist *p;
284   ca_proplist_create(&p);
285   if (!p) {
286     return NS_ERROR_OUT_OF_MEMORY;
287   }
288 
289   nsAutoCString path;
290   rv = canberraFile->GetNativePath(path);
291   if (NS_FAILED(rv)) {
292     return rv;
293   }
294 
295   ca_proplist_sets(p, "media.filename", path.get());
296   if (ca_context_play_full(ctx, 0, p, ca_finish_cb, canberraFile) >= 0) {
297     // Don't delete the temporary file here if ca_context_play_full succeeds
298     canberraFile.forget();
299   }
300   ca_proplist_destroy(p);
301 
302   return NS_OK;
303 }
304 
Beep()305 NS_IMETHODIMP nsSound::Beep() {
306   ::gdk_beep();
307   return NS_OK;
308 }
309 
Play(nsIURL * aURL)310 NS_IMETHODIMP nsSound::Play(nsIURL *aURL) {
311   if (!mInited) Init();
312 
313   if (!libcanberra) return NS_ERROR_NOT_AVAILABLE;
314 
315   bool isFile;
316   nsresult rv = aURL->SchemeIs("file", &isFile);
317   if (NS_SUCCEEDED(rv) && isFile) {
318     ca_context *ctx = ca_context_get_default();
319     if (!ctx) {
320       return NS_ERROR_OUT_OF_MEMORY;
321     }
322 
323     nsAutoCString spec;
324     rv = aURL->GetSpec(spec);
325     if (NS_FAILED(rv)) {
326       return rv;
327     }
328     gchar *path = g_filename_from_uri(spec.get(), nullptr, nullptr);
329     if (!path) {
330       return NS_ERROR_FILE_UNRECOGNIZED_PATH;
331     }
332 
333     ca_context_play(ctx, 0, "media.filename", path, nullptr);
334     g_free(path);
335   } else {
336     nsCOMPtr<nsIStreamLoader> loader;
337     rv = NS_NewStreamLoader(getter_AddRefs(loader), aURL,
338                             this,  // aObserver
339                             nsContentUtils::GetSystemPrincipal(),
340                             nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
341                             nsIContentPolicy::TYPE_OTHER);
342   }
343 
344   return rv;
345 }
346 
PlayEventSound(uint32_t aEventId)347 NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) {
348   if (!mInited) Init();
349 
350   if (!libcanberra) return NS_OK;
351 
352   // Do we even want alert sounds?
353   GtkSettings *settings = gtk_settings_get_default();
354 
355   if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
356                                    "gtk-enable-event-sounds")) {
357     gboolean enable_sounds = TRUE;
358     g_object_get(settings, "gtk-enable-event-sounds", &enable_sounds, nullptr);
359 
360     if (!enable_sounds) {
361       return NS_OK;
362     }
363   }
364 
365   ca_context *ctx = ca_context_get_default();
366   if (!ctx) {
367     return NS_ERROR_OUT_OF_MEMORY;
368   }
369 
370   switch (aEventId) {
371     case EVENT_ALERT_DIALOG_OPEN:
372       ca_context_play(ctx, 0, "event.id", "dialog-warning", nullptr);
373       break;
374     case EVENT_CONFIRM_DIALOG_OPEN:
375       ca_context_play(ctx, 0, "event.id", "dialog-question", nullptr);
376       break;
377     case EVENT_NEW_MAIL_RECEIVED:
378       ca_context_play(ctx, 0, "event.id", "message-new-email", nullptr);
379       break;
380     case EVENT_MENU_EXECUTE:
381       ca_context_play(ctx, 0, "event.id", "menu-click", nullptr);
382       break;
383     case EVENT_MENU_POPUP:
384       ca_context_play(ctx, 0, "event.id", "menu-popup", nullptr);
385       break;
386   }
387   return NS_OK;
388 }
389 
PlaySystemSound(const nsAString & aSoundAlias)390 NS_IMETHODIMP nsSound::PlaySystemSound(const nsAString &aSoundAlias) {
391   if (NS_IsMozAliasSound(aSoundAlias)) {
392     NS_WARNING(
393         "nsISound::playSystemSound is called with \"_moz_\" events, they are "
394         "obsolete, use nsISound::playEventSound instead");
395     uint32_t eventId;
396     if (aSoundAlias.Equals(NS_SYSSOUND_ALERT_DIALOG))
397       eventId = EVENT_ALERT_DIALOG_OPEN;
398     else if (aSoundAlias.Equals(NS_SYSSOUND_CONFIRM_DIALOG))
399       eventId = EVENT_CONFIRM_DIALOG_OPEN;
400     else if (aSoundAlias.Equals(NS_SYSSOUND_MAIL_BEEP))
401       eventId = EVENT_NEW_MAIL_RECEIVED;
402     else if (aSoundAlias.Equals(NS_SYSSOUND_MENU_EXECUTE))
403       eventId = EVENT_MENU_EXECUTE;
404     else if (aSoundAlias.Equals(NS_SYSSOUND_MENU_POPUP))
405       eventId = EVENT_MENU_POPUP;
406     else
407       return NS_OK;
408     return PlayEventSound(eventId);
409   }
410 
411   nsresult rv;
412   nsCOMPtr<nsIURI> fileURI;
413 
414   // create a nsIFile and then a nsIFileURL from that
415   nsCOMPtr<nsIFile> soundFile;
416   rv = NS_NewLocalFile(aSoundAlias, true, getter_AddRefs(soundFile));
417   NS_ENSURE_SUCCESS(rv, rv);
418 
419   rv = NS_NewFileURI(getter_AddRefs(fileURI), soundFile);
420   NS_ENSURE_SUCCESS(rv, rv);
421 
422   nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv);
423   NS_ENSURE_SUCCESS(rv, rv);
424 
425   rv = Play(fileURL);
426 
427   return rv;
428 }
429