/*
* Copyright (C) 2012-2016 Julien Bonjean <julien@bonjean.info>
*
* This file is part of Beluga Player.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package info.bonjean.beluga.gui.pivot;
import info.bonjean.beluga.bus.InternalBus;
import info.bonjean.beluga.bus.InternalBusSubscriber;
import info.bonjean.beluga.bus.PlaybackEvent;
import info.bonjean.beluga.client.BelugaState;
import info.bonjean.beluga.client.LastFMSession;
import info.bonjean.beluga.client.PandoraClient;
import info.bonjean.beluga.client.PandoraPlaylist;
import info.bonjean.beluga.configuration.BelugaConfiguration;
import info.bonjean.beluga.configuration.ConnectionType;
import info.bonjean.beluga.connection.BelugaHTTPClient;
import info.bonjean.beluga.exception.BelugaException;
import info.bonjean.beluga.exception.InternalException;
import info.bonjean.beluga.gui.PivotUI;
import info.bonjean.beluga.gui.notification.Notification;
import info.bonjean.beluga.response.Feedback;
import info.bonjean.beluga.response.Result;
import info.bonjean.beluga.response.SearchArtist;
import info.bonjean.beluga.response.SearchSong;
import info.bonjean.beluga.response.Song;
import info.bonjean.beluga.response.Station;
import java.awt.Desktop;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.pivot.wtk.Action;
import org.apache.pivot.wtk.ApplicationContext;
import org.apache.pivot.wtk.Checkbox;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Container;
import org.apache.pivot.wtk.Menu;
import org.apache.pivot.wtk.MenuButton;
import org.apache.pivot.wtk.Sheet;
import org.apache.pivot.wtk.SheetCloseListener;
import org.apache.pivot.wtk.content.ListItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Julien Bonjean <julien@bonjean.info>
*
* This is the main controller, that acts as a "glue" between the UI and the
* backend.
* Any idea to improve this design is welcome.
*
* TODO: more cleanup to do...
*
*/
public class UIController implements InternalBusSubscriber
{
private static Logger log = LoggerFactory.getLogger(UIController.class);
private static final BelugaState state = BelugaState.getInstance();
private static final PandoraClient pandoraClient = PandoraClient.getInstance();
private static final BelugaConfiguration configuration = BelugaConfiguration.getInstance();
private static final PandoraPlaylist playlist = PandoraPlaylist.getInstance();
private final MainWindow mainWindow;
private static PlayerUI playerUI;
private static MenuUI menuUI;
public UIController(MainWindow mainWindow)
{
InternalBus.subscribe(this);
this.mainWindow = mainWindow;
}
public void initialize()
{
playerUI = MainWindow.getInstance().playerUI;
menuUI = MainWindow.getInstance().menuUI;
}
protected void registerActions()
{
Action.getNamedActions().put("load", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final String newPage = source.getUserData().get("bxml") != null ? (String) source
.getUserData().get("bxml") : state.getPage().getName();
// load data outside of UI thread
if (newPage.equals("station"))
{
log.info("retrievingStationDetails");
// retrieve station full information
state.setStation(pandoraClient.getStation(state.getStation()));
}
else if (newPage.equals("bookmarks"))
{
log.info("retrievingBookmarks");
state.setBookmarks(pandoraClient.getBookmarks());
}
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage(newPage);
}
}, true);
}
});
Action.getNamedActions().put("debug-refresh", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.reloadResources();
mainWindow.loadPage(state.getPage().getName());
}
}, true);
}
});
Action.getNamedActions().put("back", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage(isPlaybackStarted() ? "song" : "welcome");
}
}, false);
}
});
Action.getNamedActions().put("exit", new Action()
{
@Override
public void perform(Component source)
{
System.exit(0);
}
});
Action.getNamedActions().put("open-url", new Action()
{
@Override
public void perform(Component source)
{
String url = (String) source.getUserData().get("url");
try
{
Desktop.getDesktop().browse(new URI(url));
}
catch (Exception e)
{
log.error(e.getMessage(), e);
}
}
});
Action.getNamedActions().put("delete-station", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.confirmStationDelete.open(mainWindow, new SheetCloseListener()
{
@Override
public void sheetClosed(Sheet sheet)
{
if (mainWindow.confirmStationDelete.getResult()
&& mainWindow.confirmStationDelete.getSelectedOptionIndex() == 1)
{
try
{
log.info("deletingStation");
pandoraClient.deleteStation(state.getStation());
log.info("stationDeleted");
updateStationsList();
selectStation(null);
playerUI.skip();
}
catch (BelugaException e)
{
log.error(e.getMessage(), e);
}
}
}
});
}
}, true);
}
});
Action.getNamedActions().put("void", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("noActionAssociated");
}
});
Action.getNamedActions().put("bookmark-artist", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("creatingNewBookmark");
pandoraClient.addArtistBookmark(state.getSong().getTrackToken());
log.info("artistBookmarkCreated");
}
});
Action.getNamedActions().put("bookmark-song", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("creatingNewBookmark");
pandoraClient.addSongBookmark(state.getSong().getTrackToken());
log.info("songBookmarkCreated");
}
});
Action.getNamedActions().put("start-pandora", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("connectionToPandora");
// ensure the initial state is clear
clearResources();
playerUI.close();
pandoraClient.partnerLogin();
pandoraClient.userLogin();
updateStationsList();
if (state.getStationList().size() == 0)
{
// we need at least one station, this way there is no
// need
// to handle the no station case
// default station: Wolfgang Amadeus Mozart
pandoraClient.addStation("C88");
log.info("defaultStationCreated");
updateStationsList();
}
selectStation(null);
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage("connected");
}
}, true);
// notify the player we are ready
InternalBus.publish(new PlaybackEvent(PlaybackEvent.Type.PANDORA_CONNECTED, null));
// start the player
playerUI.open();
}
});
Action.getNamedActions().put("stop-pandora", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
clearResources();
playerUI.close();
mainWindow.loadPage("welcome");
log.info("disconnectedFromPandora");
}
}, true);
}
});
Action.getNamedActions().put("next-song", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("skippingSong");
playerUI.skip();
}
});
Action.getNamedActions().put("pause-song", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
if (playerUI.isPaused())
log.info("unpausingSong");
else
log.info("pausingSong");
playerUI.playPause();
}
});
Action.getNamedActions().put("select-station", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(Component source) throws BelugaException
{
log.info("changingStation");
Station station = (Station) source.getUserData().get("station");
selectStation(station);
playerUI.skip();
}
});
Action.getNamedActions().put("like", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
log.info("sendingFeedback");
pandoraClient.addFeedback(state.getSong(), true);
log.info("feedbackSent");
}
});
Action.getNamedActions().put("ban", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
log.info("sendingFeedback");
pandoraClient.addFeedback(state.getSong(), false);
log.info("feedbackSent");
playerUI.skip();
}
});
Action.getNamedActions().put("sleep", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
log.info("sendingFeedback");
pandoraClient.sleepSong(state.getSong());
log.info("feedbackSent");
playerUI.skip();
}
});
Action.getNamedActions().put("create", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
log.info("creatingNewStation");
String type = (String) source.getUserData().get("type");
String token = (String) source.getUserData().get("token");
if (type.equals("search"))
{
log.debug("Add station from search results, token: " + token);
pandoraClient.addStation(token);
}
else
{
log.debug("Add station from " + type + ", token: " + token);
pandoraClient.addStation(type, token);
}
log.info("newStationCreated");
// update stations list
updateStationsList();
}
});
Action.getNamedActions().put("search", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final SearchUI searchUI = getPage(SearchUI.class);
String query = searchUI.searchInput.getText();
if (query.isEmpty())
{
log.info("emptyQuery");
return;
}
log.info("searching");
setEnabled(false);
final Result results = pandoraClient.search(searchUI.searchInput.getText());
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
searchUI.artistsPane.remove(0, searchUI.artistsPane.getLength());
searchUI.songsPane.remove(0, searchUI.songsPane.getLength());
for (SearchArtist artist : results.getArtists())
searchUI.artistsPane.add(searchUI.newResult(artist.getArtistName(),
artist.getArtistName(), artist.getMusicToken(), "search"));
for (SearchSong artist : results.getSongs())
searchUI.songsPane.add(searchUI.newResult(artist.getSongName() + " ("
+ artist.getArtistName() + ")", artist.getSongName(),
artist.getMusicToken(), "search"));
searchUI.nearMatchesAvailable.setVisible(results.isNearMatchesAvailable());
setEnabled(true);
}
}, true);
}
@Override
public void afterPerform()
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
try
{
SearchUI searchUI = getPage(SearchUI.class);
searchUI.setTabTitles();
searchUI.setFocus();
}
catch (BelugaException e)
{
log.error(e.getMessage(), e);
}
}
}, true);
}
});
Action.getNamedActions().put("update-quickmix", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final List<String> quickMixStationIds = new ArrayList<String>();
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
QuickMixUI quickMixUI;
try
{
quickMixUI = getPage(QuickMixUI.class);
}
catch (BelugaException e)
{
log.error(e.getMessage(), e);
return;
}
for (int i = 0; i < quickMixUI.stationsPane.getLength(); i++)
{
Checkbox station = (Checkbox) quickMixUI.stationsPane.get(i);
if (station.isSelected())
quickMixStationIds.add((String) station.getUserData().get(
"stationId"));
}
}
}, true);
pandoraClient.setQuickMix(quickMixStationIds);
// update stations to get the new quickmix station configuration
updateStationsList();
if (state.getStation().isQuickMix())
playlist.clear();
// redirect to the main screen
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage("song");
}
}, false);
}
});
Action.getNamedActions().put("create-account", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
AccountCreationUI accountCreationUI = getPage(AccountCreationUI.class);
if (!accountCreationUI.termsOfUseInput.isSelected())
{
log.error("youMustAgreeToTheTermsOfUse");
return;
}
log.info("creatingNewAccount");
pandoraClient.partnerLogin();
pandoraClient.createUser(
accountCreationUI.emailAddressInput.getText(),
accountCreationUI.passwordInput.getText(),
accountCreationUI.birthYearInput.getText(),
accountCreationUI.zipCodeInput.getText(),
(String) accountCreationUI.genderGroup.getSelection().getUserData()
.get("value"),
String.valueOf(accountCreationUI.emailOptInInput.isSelected()));
log.info("accountCreated");
configuration.setUserName(accountCreationUI.emailAddressInput.getText());
configuration.setPassword(accountCreationUI.passwordInput.getText());
configuration.setConfigurationVersion(state.getVersion());
configuration.store();
}
});
Action.getNamedActions().put("delete-bookmark", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final BookmarksUI bookmarksUI = getPage(BookmarksUI.class);
log.info("deletingBookmark");
final String bookmarkToken = (String) source.getUserData().get("bookmarkToken");
final MenuButton item = (MenuButton) source.getUserData().get("item");
final String type = (String) source.getUserData().get("type");
if (type.equals("song"))
pandoraClient.deleteSongBookmark(bookmarkToken);
else
pandoraClient.deleteArtistBookmark(bookmarkToken);
// update UI
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
if (type.equals("song"))
bookmarksUI.songBookmarksPane.remove(item);
else
bookmarksUI.artistBookmarksPane.remove(item);
}
}, true);
log.info("bookmarkDeleted");
}
});
Action.getNamedActions().put("delete-feedback", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final StationUI stationUI = getPage(StationUI.class);
log.info("deletingFeedback");
final Feedback feedback = (Feedback) source.getUserData().get("feedback");
final MenuButton item = (MenuButton) source.getUserData().get("item");
pandoraClient.deleteFeedback(feedback.getFeedbackId());
// update song currently playing if necessary
if (feedback.isPositive())
stationUI.updateSongFeedback(feedback.getFeedbackId());
// update UI
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
if (feedback.isPositive())
stationUI.lovedSongsPane.remove(item);
else
stationUI.bannedSongsPane.remove(item);
}
}, true);
log.info("feedbackDeleted");
}
});
Action.getNamedActions().put("save-preferences", new AsyncAction(mainWindow)
{
@Override
public void asyncPerform(final Component source) throws BelugaException
{
final PreferencesUI preferencesUI = getPage(PreferencesUI.class);
configuration.setUserName(preferencesUI.emailAddressInput.getText());
configuration.setPassword(preferencesUI.passwordInput.getText());
configuration.setConnectionType((ConnectionType) preferencesUI.connectionType
.getSelectedItem());
configuration.setProxyHost(preferencesUI.httpProxyHostInput.getText());
configuration.setProxyPort(preferencesUI.httpProxyPortInput.getText());
configuration.setLastFMEnabled(preferencesUI.lastFMEnableCheckbox.isSelected());
configuration.setLastFMUsername(preferencesUI.lastFMUsernameInput.getText());
configuration.setLastFMPassword(preferencesUI.lastFMPasswordInput.getText());
configuration.setAdsDetectionEnabled(preferencesUI.adsEnableDetectionCheckbox
.isSelected());
configuration.setAdsSilenceEnabled(preferencesUI.adsEnableSilentCheckbox
.isSelected());
configuration
.setNotificationsStyle((String) ((ListItem) preferencesUI.notificationsStyle
.getSelectedItem()).getUserData());
configuration.setStationsOrderBy((String) ((ListItem) preferencesUI.stationsOrderBy
.getSelectedItem()).getUserData());
configuration.setWindowRestoreEnabled(preferencesUI.windowRestoreCheckbox
.isSelected());
if (!state.getVersion().equals(BelugaConfiguration.CONFIGURATION_DEFAULT_VERSION))
configuration.setConfigurationVersion(state.getVersion());
configuration.store();
log.info("preferencesUpdated");
BelugaHTTPClient.reset();
LastFMSession.reset();
// redirect to the main screen: song if playback started,
// welcome otherwise
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage(isPlaybackStarted() ? "song" : "welcome");
}
}, false);
}
});
}
@SuppressWarnings("unchecked")
private <E extends Component> E getPage(Class<E> clazz) throws BelugaException
{
Component page = state.getPage().getComponent();
if (clazz.isInstance(page))
return (E) page;
throw new InternalException("invalidActionCall");
}
private void clearResources()
{
state.reset();
pandoraClient.reset();
playlist.clear();
BelugaHTTPClient.reset();
LastFMSession.reset();
}
private void selectStation(Station newStation) throws BelugaException
{
// if no station requested, select configuration one
if (newStation == null)
{
for (Station station : state.getStationList())
{
if (station.getStationId().equals(configuration.getDefaultStationId()))
{
newStation = station;
break;
}
}
}
// else check if station requested is valid
else
{
boolean found = false;
for (Station station : state.getStationList())
{
if (station.getStationId().equals(newStation.getStationId()))
{
found = true;
break;
}
}
if (!found)
{
log.warn("requestedStationDoesNotExist");
newStation = null;
}
}
// at this point, if no station has been selected, there has been a
// problem, select first one
if (newStation == null)
{
if (!state.getStationList().isEmpty())
newStation = state.getStationList().get(0);
else
{
// should not happen
log.error("noStation");
return;
}
}
// check if station changed
if (state.getStation() == null
|| !newStation.getStationId().equals(state.getStation().getStationId()))
{
// invalidate playlist
playlist.clear();
// update the configuration
configuration.setDefaultStationId(newStation.getStationId());
configuration.store();
}
state.setStation(newStation);
log.debug("Station selected: " + state.getStation().getStationName());
// initially feed the playlist
playlist.feedQueue();
}
private void updateStationsList() throws BelugaException
{
log.info("retrievingStations");
List<Station> stationList = pandoraClient.getStationList();
if (configuration.getStationsOrderBy().equals("name"))
{
Collections.sort(stationList, new Comparator<Station>()
{
@Override
public int compare(Station station1, Station station2)
{
return station1.getStationName().compareTo(station2.getStationName());
}
});
}
state.setStationList(stationList);
}
private boolean isPlaybackStarted()
{
return pandoraClient.isLoggedIn() && state.getSong() != null;
}
private void recursiveEnableComponent(Component component, boolean enabled)
{
// disable the parent component first to prevent any user interaction
if (!enabled)
PivotUI.enableComponent(component, false);
// if it's a container, handle the children (except for menu)
if (component instanceof Container && !(component instanceof Menu))
{
Iterator<Component> iterator = ((Container) component).iterator();
while (iterator.hasNext())
recursiveEnableComponent(iterator.next(), enabled);
}
// enable the parent component
if (enabled)
PivotUI.enableComponent(component, true);
}
/*
* Keep everything related to enabled/disabled state here, it make it easier
* to ensure consistency between screens
*/
public void enableUI(boolean enabled)
{
if (menuUI == null || mainWindow == null || playerUI == null)
return;
// because of the Component class sealing, we cannot override it
// so we need to manually update enable state of all components
recursiveEnableComponent(menuUI, enabled);
Iterator<Component> contentIterator = mainWindow.contentWrapper.iterator();
while (contentIterator.hasNext())
recursiveEnableComponent(contentIterator.next(), enabled);
// get Pandora status
boolean connected = pandoraClient.isLoggedIn();
// enable/disable the loading and disconnected icons
mainWindow.statusBarIconDiconnected.setVisible(!connected);
mainWindow.loader.setVisible(!enabled);
// enable/disable Pandora related features if connected
if (enabled && connected)
{
// disable delete station button if only 1 station (quickmix) or
// if quickmix is selected
if (state.getStationList().size() < 3
|| (state.getStation() != null && state.getStation().isQuickMix()))
PivotUI.enableComponent(mainWindow.deleteStationButton, false);
else
PivotUI.enableComponent(mainWindow.deleteStationButton, true);
// there is no station details for the quickmix
if (state.getStation() != null && state.getStation().isQuickMix())
PivotUI.enableComponent(mainWindow.stationDetailsButton, false);
else
PivotUI.enableComponent(mainWindow.stationDetailsButton, true);
}
mainWindow.pandoraMenu.setEnabled(connected);
mainWindow.stationsSearch.setEnabled(connected);
// update player UI
recursiveEnableComponent(playerUI, !playerUI.isClosed());
}
@Override
public void receive(PlaybackEvent playbackEvent)
{
log.debug("Received event: " + playbackEvent.getType());
Song song = playbackEvent.getSong();
switch (playbackEvent.getType())
{
case SONG_START:
// reload song page only if currently displayed
if (state.getPage().getName().equals("song")
|| state.getPage().getName().equals("connected"))
{
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage("song");
}
}, false);
}
if (!configuration.getNotificationsStyle().equals("disabled"))
// send a desktop notification
new Notification(state.getSong());
break;
case SONG_FINISH:
log.debug("Played " + song.getPosition() + " of " + song.getDuration());
if (configuration.getLastFMEnabled())
// scrobble with last.fm
LastFMSession.getInstance().scrobbleTrack(song);
break;
case PANDORA_DISCONNECTED:
clearResources();
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
mainWindow.loadPage("welcome");
}
}, true);
log.info("disconnectedFromPandora");
break;
case PANDORA_CONNECTED:
case SONG_PAUSE:
case SONG_RESUME:
}
ApplicationContext.queueCallback(new Runnable()
{
@Override
public void run()
{
// update player UI
recursiveEnableComponent(playerUI, !playerUI.isClosed());
}
}, false);
}
}