1/*
2 * MainApp.cpp - Media Player for the Haiku Operating System
3 *
4 * Copyright (C) 2006 Marcus Overhagen <marcus@overhagen.de>
5 * Copyright (C) 2008 Stephan Aßmus <superstippi@gmx.de> (MIT Ok)
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * version 2 as published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 *
20 */
21
22
23#include "MainApp.h"
24
25#include <Alert.h>
26#include <Autolock.h>
27#include <Catalog.h>
28#include <Entry.h>
29#include <FilePanel.h>
30#include <Locale.h>
31#include <MediaDefs.h>
32#include <MediaRoster.h>
33#include <MimeType.h>
34#include <Path.h>
35#include <Resources.h>
36#include <Roster.h>
37
38#include <stdio.h>
39#include <stdlib.h>
40#include <unistd.h>
41
42#include "EventQueue.h"
43#include "Playlist.h"
44#include "Settings.h"
45#include "SettingsWindow.h"
46
47
48#undef B_TRANSLATION_CONTEXT
49#define B_TRANSLATION_CONTEXT "MediaPlayer-Main"
50
51
52static const char* kCurrentPlaylistFilename = "MediaPlayer Current Playlist";
53
54const char* kAppSig = "application/x-vnd.Haiku-MediaPlayer";
55
56MainApp* gMainApp;
57
58static const char* kMediaServerSig = B_MEDIA_SERVER_SIGNATURE;
59static const char* kMediaServerAddOnSig = "application/x-vnd.Be.addon-host";
60
61
62MainApp::MainApp()
63	:
64	BApplication(kAppSig),
65	fPlayerCount(0),
66	fSettingsWindow(NULL),
67
68	fOpenFilePanel(NULL),
69	fSaveFilePanel(NULL),
70	fLastFilePanelFolder(),
71
72	fMediaServerRunning(false),
73	fMediaAddOnServerRunning(false),
74
75	fAudioWindowFrameSaved(false),
76	fLastSavedAudioWindowCreationTime(0)
77{
78	fLastFilePanelFolder = Settings::Default()->FilePanelFolder();
79
80	// Now tell the application roster, that we're interested
81	// in getting notifications of apps being launched or quit.
82	// In this way we are going to detect a media_server restart.
83	be_roster->StartWatching(BMessenger(this, this),
84		B_REQUEST_LAUNCHED | B_REQUEST_QUIT);
85	// we will keep track of the status of media_server
86	// and media_addon_server
87	fMediaServerRunning = be_roster->IsRunning(kMediaServerSig);
88	fMediaAddOnServerRunning = be_roster->IsRunning(kMediaServerAddOnSig);
89
90	if (!fMediaServerRunning || !fMediaAddOnServerRunning) {
91		BAlert* alert = new BAlert("start_media_server",
92			B_TRANSLATE("It appears the media server is not running.\n"
93			"Would you like to start it ?"), B_TRANSLATE("Quit"),
94			B_TRANSLATE("Start media server"), NULL,
95			B_WIDTH_AS_USUAL, B_WARNING_ALERT);
96		alert->SetShortcut(0, B_ESCAPE);
97
98		if (alert->Go() == 0) {
99			PostMessage(B_QUIT_REQUESTED);
100			return;
101		}
102
103		launch_media_server();
104
105		fMediaServerRunning = be_roster->IsRunning(kMediaServerSig);
106		fMediaAddOnServerRunning = be_roster->IsRunning(kMediaServerAddOnSig);
107	}
108}
109
110
111MainApp::~MainApp()
112{
113	delete fOpenFilePanel;
114	delete fSaveFilePanel;
115}
116
117
118bool
119MainApp::QuitRequested()
120{
121	// Make sure we store the current playlist, if applicable.
122	for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
123		MainWin* playerWindow = dynamic_cast<MainWin*>(window);
124		if (playerWindow == NULL)
125			continue;
126
127		BAutolock _(playerWindow);
128
129		BMessage quitMessage;
130		playerWindow->GetQuitMessage(&quitMessage);
131
132		// Store the playlist if there is one. If the user has multiple
133		// instances playing audio at the this time, the first instance wins.
134		BMessage playlistArchive;
135		if (quitMessage.FindMessage("playlist", &playlistArchive) == B_OK) {
136			_StoreCurrentPlaylist(&playlistArchive);
137			break;
138		}
139	}
140
141	// Note: This needs to be done here, SettingsWindow::QuitRequested()
142	// returns "false" always. (Standard BApplication quit procedure will
143	// hang otherwise.)
144	if (fSettingsWindow && fSettingsWindow->Lock())
145		fSettingsWindow->Quit();
146	fSettingsWindow = NULL;
147
148	// store the current file panel ref in the global settings
149	Settings::Default()->SetFilePanelFolder(fLastFilePanelFolder);
150
151	return BApplication::QuitRequested();
152}
153
154
155MainWin*
156MainApp::NewWindow(BMessage* message)
157{
158	BAutolock _(this);
159	fPlayerCount++;
160	return new(std::nothrow) MainWin(fPlayerCount == 1, message);
161}
162
163
164int32
165MainApp::PlayerCount() const
166{
167	BAutolock _(const_cast<MainApp*>(this));
168	return fPlayerCount;
169}
170
171
172// #pragma mark -
173
174
175void
176MainApp::ReadyToRun()
177{
178	// make sure we have at least one window open
179	if (fPlayerCount == 0) {
180		MainWin* window = NewWindow();
181		if (window == NULL) {
182			PostMessage(B_QUIT_REQUESTED);
183			return;
184		}
185		BMessage lastPlaylistArchive;
186		if (_RestoreCurrentPlaylist(&lastPlaylistArchive) == B_OK) {
187			lastPlaylistArchive.what = M_OPEN_PREVIOUS_PLAYLIST;
188			window->PostMessage(&lastPlaylistArchive);
189		} else
190			window->Show();
191	}
192
193	// setup the settings window now, we need to have it
194	fSettingsWindow = new SettingsWindow(BRect(150, 150, 450, 520));
195	fSettingsWindow->Hide();
196	fSettingsWindow->Show();
197
198	_InstallPlaylistMimeType();
199}
200
201
202void
203MainApp::RefsReceived(BMessage* message)
204{
205	// The user dropped a file (or files) on this app's icon,
206	// or double clicked a file that's handled by this app.
207	// Command line arguments are also redirected to here by
208	// ArgvReceived() but without MIME type check.
209
210	// If multiple refs are received in short succession we
211	// combine them into a single window/playlist. Tracker
212	// will send multiple messages when opening a multi-
213	// selection for example and we don't want to spawn large
214	// numbers of windows when someone just tries to open an
215	// album. We use half a second time and prolong it for
216	// each new ref received.
217	static bigtime_t sLastRefsReceived = 0;
218	static MainWin* sLastRefsWindow = NULL;
219
220	if (system_time() - sLastRefsReceived < 500000) {
221		// Find the last opened window
222		for (int32 i = CountWindows() - 1; i >= 0; i--) {
223			MainWin* playerWindow = dynamic_cast<MainWin*>(WindowAt(i));
224			if (playerWindow == NULL)
225				continue;
226
227			if (playerWindow != sLastRefsWindow) {
228				// The window has changed since the last refs
229				sLastRefsReceived = 0;
230				sLastRefsWindow = NULL;
231				break;
232			}
233
234			message->AddBool("append to playlist", true);
235			playerWindow->PostMessage(message);
236			sLastRefsReceived = system_time();
237			return;
238		}
239	}
240
241	sLastRefsWindow = NewWindow(message);
242	sLastRefsReceived = system_time();
243}
244
245
246void
247MainApp::ArgvReceived(int32 argc, char** argv)
248{
249	char cwd[B_PATH_NAME_LENGTH];
250	getcwd(cwd, sizeof(cwd));
251
252	BMessage message(B_REFS_RECEIVED);
253
254	for (int i = 1; i < argc; i++) {
255		BPath path;
256		if (argv[i][0] != '/')
257			path.SetTo(cwd, argv[i]);
258		else
259			path.SetTo(argv[i]);
260		BEntry entry(path.Path(), true);
261		if (!entry.Exists() || !entry.IsFile())
262			continue;
263
264		entry_ref ref;
265		if (entry.GetRef(&ref) == B_OK)
266			message.AddRef("refs", &ref);
267	}
268
269	if (message.HasRef("refs"))
270		RefsReceived(&message);
271}
272
273
274void
275MainApp::MessageReceived(BMessage* message)
276{
277	switch (message->what) {
278		case M_NEW_PLAYER:
279		{
280			MainWin* window = NewWindow();
281			if (window != NULL)
282				window->Show();
283			break;
284		}
285		case M_PLAYER_QUIT:
286		{
287			// store the window settings of this instance
288			MainWin* window = NULL;
289			bool audioOnly = false;
290			BRect windowFrame;
291			bigtime_t creationTime;
292			if (message->FindPointer("instance", (void**)&window) == B_OK
293				&& message->FindBool("audio only", &audioOnly) == B_OK
294				&& message->FindRect("window frame", &windowFrame) == B_OK
295				&& message->FindInt64("creation time", &creationTime) == B_OK) {
296				if (audioOnly && (!fAudioWindowFrameSaved
297						|| creationTime < fLastSavedAudioWindowCreationTime)) {
298					fAudioWindowFrameSaved = true;
299					fLastSavedAudioWindowCreationTime = creationTime;
300
301					Settings::Default()->SetAudioPlayerWindowFrame(windowFrame);
302				}
303			}
304
305			// Store the playlist if there is one. Since the app is doing
306			// this, it is "atomic". If the user has multiple instances
307			// playing audio at the same time, the last instance which is
308			// quit wins.
309			BMessage playlistArchive;
310			if (message->FindMessage("playlist", &playlistArchive) == B_OK)
311				_StoreCurrentPlaylist(&playlistArchive);
312
313			// quit if this was the last player window
314			fPlayerCount--;
315			if (fPlayerCount == 0)
316				PostMessage(B_QUIT_REQUESTED);
317			break;
318		}
319
320		case B_SOME_APP_LAUNCHED:
321		case B_SOME_APP_QUIT:
322		{
323			const char* mimeSig;
324			if (message->FindString("be:signature", &mimeSig) < B_OK)
325				break;
326
327			bool isMediaServer = strcmp(mimeSig, kMediaServerSig) == 0;
328			bool isAddonServer = strcmp(mimeSig, kMediaServerAddOnSig) == 0;
329			if (!isMediaServer && !isAddonServer)
330				break;
331
332			bool running = (message->what == B_SOME_APP_LAUNCHED);
333			if (isMediaServer)
334				fMediaServerRunning = running;
335			if (isAddonServer)
336				fMediaAddOnServerRunning = running;
337
338			if (!fMediaServerRunning && !fMediaAddOnServerRunning) {
339				fprintf(stderr, "media server has quit.\n");
340				// trigger closing of media nodes
341				BMessage broadcast(M_MEDIA_SERVER_QUIT);
342				_BroadcastMessage(broadcast);
343			} else if (fMediaServerRunning && fMediaAddOnServerRunning) {
344				fprintf(stderr, "media server has launched.\n");
345				// HACK!
346				// quit our now invalid instance of the media roster
347				// so that before new nodes are created,
348				// we get a new roster (it is a normal looper)
349				// TODO: This functionality could become part of
350				// BMediaRoster. It could detect the start/quit of
351				// the servers like it is done here, and either quit
352				// itself, or re-establish the connection, and send some
353				// notification to the app... something along those lines.
354				BMediaRoster* roster = BMediaRoster::CurrentRoster();
355				if (roster) {
356					roster->Lock();
357					roster->Quit();
358				}
359				// give the servers some time to init...
360				snooze(3000000);
361				// trigger re-init of media nodes
362				BMessage broadcast(M_MEDIA_SERVER_STARTED);
363				_BroadcastMessage(broadcast);
364			}
365			break;
366		}
367		case M_SETTINGS:
368			_ShowSettingsWindow();
369			break;
370
371		case M_SHOW_OPEN_PANEL:
372			_ShowOpenFilePanel(message);
373			break;
374		case M_SHOW_SAVE_PANEL:
375			_ShowSaveFilePanel(message);
376			break;
377
378		case M_OPEN_PANEL_RESULT:
379			_HandleOpenPanelResult(message);
380			break;
381		case M_SAVE_PANEL_RESULT:
382			_HandleSavePanelResult(message);
383			break;
384		case B_CANCEL:
385		{
386			// The user canceled a file panel, but store at least the current
387			// file panel folder.
388			uint32 oldWhat;
389			if (message->FindInt32("old_what", (int32*)&oldWhat) != B_OK)
390				break;
391			if (oldWhat == M_OPEN_PANEL_RESULT && fOpenFilePanel != NULL)
392				fOpenFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
393			else if (oldWhat == M_SAVE_PANEL_RESULT && fSaveFilePanel != NULL)
394				fSaveFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
395			break;
396		}
397
398		default:
399			BApplication::MessageReceived(message);
400			break;
401	}
402}
403
404
405// #pragma mark -
406
407
408void
409MainApp::_BroadcastMessage(const BMessage& _message)
410{
411	for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
412		BMessage message(_message);
413		window->PostMessage(&message);
414	}
415}
416
417
418void
419MainApp::_ShowSettingsWindow()
420{
421	BAutolock lock(fSettingsWindow);
422	if (!lock.IsLocked())
423		return;
424
425	// If the window is already showing, don't jerk the workspaces around,
426	// just pull it to the current one.
427	uint32 workspace = 1UL << (uint32)current_workspace();
428	uint32 windowWorkspaces = fSettingsWindow->Workspaces();
429	if ((windowWorkspaces & workspace) == 0) {
430		// window in a different workspace, reopen in current
431		fSettingsWindow->SetWorkspaces(workspace);
432	}
433
434	if (fSettingsWindow->IsHidden())
435		fSettingsWindow->Show();
436	else
437		fSettingsWindow->Activate();
438}
439
440
441// #pragma mark - file panels
442
443
444void
445MainApp::_ShowOpenFilePanel(const BMessage* message)
446{
447	if (fOpenFilePanel == NULL) {
448		BMessenger target(this);
449		fOpenFilePanel = new BFilePanel(B_OPEN_PANEL, &target);
450	}
451
452	_ShowFilePanel(fOpenFilePanel, M_OPEN_PANEL_RESULT, message,
453		B_TRANSLATE("Open"), B_TRANSLATE("Open"));
454}
455
456
457void
458MainApp::_ShowSaveFilePanel(const BMessage* message)
459{
460	if (fSaveFilePanel == NULL) {
461		BMessenger target(this);
462		fSaveFilePanel = new BFilePanel(B_SAVE_PANEL, &target);
463	}
464
465	_ShowFilePanel(fSaveFilePanel, M_SAVE_PANEL_RESULT, message,
466		B_TRANSLATE("Save"), B_TRANSLATE("Save"));
467}
468
469
470void
471MainApp::_ShowFilePanel(BFilePanel* panel, uint32 command,
472	const BMessage* message, const char* defaultTitle,
473	const char* defaultLabel)
474{
475//	printf("_ShowFilePanel()\n");
476//	message->PrintToStream();
477
478	BMessage panelMessage(command);
479
480	if (message != NULL) {
481		BMessage targetMessage;
482		if (message->FindMessage("message", &targetMessage) == B_OK)
483			panelMessage.AddMessage("message", &targetMessage);
484
485		BMessenger target;
486		if (message->FindMessenger("target", &target) == B_OK)
487			panelMessage.AddMessenger("target", target);
488
489		const char* panelTitle;
490		if (message->FindString("title", &panelTitle) != B_OK)
491			panelTitle = defaultTitle;
492		{
493			BString finalPanelTitle = "MediaPlayer: ";
494			finalPanelTitle << panelTitle;
495			BAutolock lock(panel->Window());
496			panel->Window()->SetTitle(finalPanelTitle.String());
497		}
498		const char* buttonLabel;
499		if (message->FindString("label", &buttonLabel) != B_OK)
500			buttonLabel = defaultLabel;
501		panel->SetButtonLabel(B_DEFAULT_BUTTON, buttonLabel);
502	}
503
504//	panelMessage.PrintToStream();
505	panel->SetMessage(&panelMessage);
506
507	if (fLastFilePanelFolder != entry_ref()) {
508		panel->SetPanelDirectory(&fLastFilePanelFolder);
509	}
510
511	panel->Show();
512}
513
514
515void
516MainApp::_HandleOpenPanelResult(const BMessage* message)
517{
518	_HandleFilePanelResult(fOpenFilePanel, message);
519}
520
521
522void
523MainApp::_HandleSavePanelResult(const BMessage* message)
524{
525	_HandleFilePanelResult(fSaveFilePanel, message);
526}
527
528
529void
530MainApp::_HandleFilePanelResult(BFilePanel* panel, const BMessage* message)
531{
532//	printf("_HandleFilePanelResult()\n");
533//	message->PrintToStream();
534
535	panel->GetPanelDirectory(&fLastFilePanelFolder);
536
537	BMessage targetMessage;
538	if (message->FindMessage("message", &targetMessage) != B_OK)
539		targetMessage.what = message->what;
540
541	BMessenger target;
542	if (message->FindMessenger("target", &target) != B_OK) {
543		if (targetMessage.what == M_OPEN_PANEL_RESULT
544			|| targetMessage.what == M_SAVE_PANEL_RESULT) {
545			// prevent endless message cycle
546			return;
547		}
548		// send result message to ourselves
549		target = BMessenger(this);
550	}
551
552	// copy the important contents of the message
553	// save panel
554	entry_ref directory;
555	if (message->FindRef("directory", &directory) == B_OK)
556		targetMessage.AddRef("directory", &directory);
557	const char* name;
558	if (message->FindString("name", &name) == B_OK)
559		targetMessage.AddString("name", name);
560	// open panel
561	entry_ref ref;
562	for (int32 i = 0; message->FindRef("refs", i, &ref) == B_OK; i++)
563		targetMessage.AddRef("refs", &ref);
564
565	target.SendMessage(&targetMessage);
566}
567
568
569void
570MainApp::_StoreCurrentPlaylist(const BMessage* message) const
571{
572	BPath path;
573	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
574		|| path.Append(kCurrentPlaylistFilename) != B_OK) {
575		return;
576	}
577
578	BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
579	if (file.InitCheck() != B_OK)
580		return;
581
582	message->Flatten(&file);
583}
584
585
586status_t
587MainApp::_RestoreCurrentPlaylist(BMessage* message) const
588{
589	BPath path;
590	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
591		|| path.Append(kCurrentPlaylistFilename) != B_OK) {
592		return B_ERROR;
593	}
594
595	BFile file(path.Path(), B_READ_ONLY);
596	if (file.InitCheck() != B_OK)
597		return B_ERROR;
598
599	return message->Unflatten(&file);
600}
601
602
603void
604MainApp::_InstallPlaylistMimeType()
605{
606	// install mime type of documents
607	BMimeType mime(kBinaryPlaylistMimeString);
608	status_t ret = mime.InitCheck();
609	if (ret != B_OK) {
610		fprintf(stderr, "Could not init native document mime type (%s): %s.\n",
611			kBinaryPlaylistMimeString, strerror(ret));
612		return;
613	}
614
615	if (mime.IsInstalled() && !(modifiers() & B_SHIFT_KEY)) {
616		// mime is already installed, and the user is not
617		// pressing the shift key to force a re-install
618		return;
619	}
620
621	ret = mime.Install();
622	if (ret != B_OK && ret != B_FILE_EXISTS) {
623		fprintf(stderr, "Could not install native document mime type (%s): %s.\n",
624			kBinaryPlaylistMimeString, strerror(ret));
625		return;
626	}
627	// set preferred app
628	ret = mime.SetPreferredApp(kAppSig);
629	if (ret != B_OK) {
630		fprintf(stderr, "Could not set native document preferred app: %s\n",
631			strerror(ret));
632	}
633
634	// set descriptions
635	ret = mime.SetShortDescription("MediaPlayer playlist");
636	if (ret != B_OK) {
637		fprintf(stderr, "Could not set short description of mime type: %s\n",
638			strerror(ret));
639	}
640	ret = mime.SetLongDescription("MediaPlayer binary playlist file");
641	if (ret != B_OK) {
642		fprintf(stderr, "Could not set long description of mime type: %s\n",
643			strerror(ret));
644	}
645
646	// set extensions
647	BMessage message('extn');
648	message.AddString("extensions", "playlist");
649	ret = mime.SetFileExtensions(&message);
650	if (ret != B_OK) {
651		fprintf(stderr, "Could not set extensions of mime type: %s\n",
652			strerror(ret));
653	}
654
655	// set sniffer rule
656	char snifferRule[32];
657	uint32 bigEndianMagic = B_HOST_TO_BENDIAN_INT32(kPlaylistMagicBytes);
658	sprintf(snifferRule, "0.9 ('%4s')", (const char*)&bigEndianMagic);
659	ret = mime.SetSnifferRule(snifferRule);
660	if (ret != B_OK) {
661		BString parseError;
662		BMimeType::CheckSnifferRule(snifferRule, &parseError);
663		fprintf(stderr, "Could not set sniffer rule of mime type: %s\n",
664			parseError.String());
665	}
666
667	// set playlist icon
668	BResources* resources = AppResources();
669		// does not need to be freed (belongs to BApplication base)
670	if (resources != NULL) {
671		size_t size;
672		const void* iconData = resources->LoadResource('VICN', "PlaylistIcon",
673			&size);
674		if (iconData != NULL && size > 0) {
675			if (mime.SetIcon(reinterpret_cast<const uint8*>(iconData), size)
676				!= B_OK) {
677				fprintf(stderr, "Could not set vector icon of mime type.\n");
678			}
679		} else {
680			fprintf(stderr, "Could not find icon in app resources "
681				"(data: %p, size: %ld).\n", iconData, size);
682		}
683	} else
684		fprintf(stderr, "Could not find app resources.\n");
685}
686
687
688// #pragma mark - main
689
690
691int
692main()
693{
694	EventQueue::CreateDefault();
695
696	srand(system_time());
697
698	gMainApp = new MainApp;
699	gMainApp->Run();
700	delete gMainApp;
701
702	EventQueue::DeleteDefault();
703
704	return 0;
705}
706