package com.jbidwatcher.app;
import com.cyberfox.util.platform.Platform;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.jbidwatcher.util.PauseManager;
import com.jbidwatcher.util.queue.*;
import com.jbidwatcher.util.Constants;
import com.jbidwatcher.util.config.JConfig;
import com.jbidwatcher.util.html.JHTMLOutput;
import com.jbidwatcher.ui.util.OptionUI;
import com.jbidwatcher.ui.*;
import com.jbidwatcher.auction.AuctionEntry;
import com.jbidwatcher.auction.EntryCorral;
import com.jbidwatcher.auction.server.AuctionStats;
import com.jbidwatcher.auction.server.AuctionServerManager;
import com.jbidwatcher.auction.server.AuctionServer;
import com.jbidwatcher.search.SearchManager;
import com.jbidwatcher.UpdaterEntry;
import com.jbidwatcher.UpdateManager;
import javax.swing.*;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.util.*;
import static com.jbidwatcher.util.UIConstants.*;
/**
* User: Morgan
* Date: Mar 9, 2008
* Time: 1:23:58 AM
*
* The class that handles most of the queued UI messages.
*/
@Singleton
public final class UIBackbone implements MessageQueue.Listener {
private boolean _userValid;
private Date mNow = new Date();
private Calendar mCal = new GregorianCalendar();
private MacFriendlyFrame mFrame;
private boolean mSmall;
private final AuctionServerManager serverManager;
private final AuctionsManager auctionsManager;
private final SearchManager searcher;
private final EntryCorral entryCorral;
private final PauseManager pauseManager;
private final JBidToolBar toolBar;
@Inject
public UIBackbone(AuctionServerManager serverManager, AuctionsManager auctionsManager, SearchManager searcher,
EntryCorral entryCorral, PauseManager pauseManager, JBidToolBar toolBar) {
this.serverManager = serverManager;
this.auctionsManager = auctionsManager;
this.searcher = searcher;
this.entryCorral = entryCorral;
this.pauseManager = pauseManager;
this.toolBar = toolBar;
TimerHandler clockTimer = new TimerHandler(new TimerHandler.WakeupProcess() {
/**
* Check and update the clock every second; also handles recognition of sleep-based slippage of time.
*
* @return True always.
*/
public boolean check() {
checkClock();
return true;
}
});
clockTimer.setName("Clock");
clockTimer.start();
MQFactory.addQueue("Swing", new SwingMessageQueue());
MQFactory.getConcrete("Swing").registerListener(this);
}
private boolean _linkUp = true;
/**
* @brief Function to let any class tell us that the link is down or
* up again.
*
* @param linkIsUp Is the connection to the auction server up or down?
*/
public void setLinkUp(boolean linkIsUp) {
_linkUp = linkIsUp;
}
/**
* @brief Function to identify if the link is up or down.
*
* @return - true if the connection with the auction server appears to be working, false otherwise.
*/
public boolean getLinkUp() {
return _linkUp;
}
/**
* Handle messages to tell the UI to do something.
* <br>
* <br>
* This is the sole place that UI updates should be done, and all
* requests to do UI activities should be sent via the MessageQueue
* for "Swing". This ensures that they are done on the Swing UI
* update thread, instead of in random threads throughout the
* program. Since Swing is single-threaded, this is necessary.
* <br>
* <br>
* Anything else is presumed to be a status message, to be displayed
* in the status bar at the bottom of the screen.
* <br>
* <br>
* The messages supported (suffixed by _MSG) are partially documented in
* {@link com.jbidwatcher.util.UIConstants}
*
* @param deQ A string containing a command to be processed by the UI.
*/
public void messageAction(Object deQ) {
String[] cmdMessage = ((String) deQ).split(" ", 2);
switch(cmdMessage[0]) {
case QUIT_MSG:
logActivity("Shutting down.");
mFrame.shutdown();
break;
case HIDE_MSG:
hideUI();
break;
case RESTORE_MSG:
showUI();
break;
case VISIBILITY_MSG:
toggleVisibility();
break;
case SNIPE_ALTERED_MSG:
alterSnipeStatus();
break;
case NEWVERSION_MSG:
logActivity("New version found!");
announceNewVersion();
break;
case SMALL_USERINFO:
mSmall = !mSmall;
break;
case START_UPDATING:
startUpdating();
break;
case VALID_LOGIN_MSG:
handleValidLogin();
break;
case NO_NEWVERSION_MSG:
JOptionPane.showMessageDialog(null, "No new version available yet.\nKeep checking back!", "No new version", JOptionPane.PLAIN_MESSAGE);
break;
case BAD_NEWVERSION_MSG:
JOptionPane.showMessageDialog(null, "Failed to check for a new version\nProbably a temporary network issue; try again in a little while.",
"Version check failed", JOptionPane.PLAIN_MESSAGE);
break;
case TOOLBAR_MSG:
toolBar.togglePanel();
break;
case HEADER_MSG:
String headerMsg = cmdMessage[1];
handleHeader(headerMsg);
break;
case LOGIN_STATUS_MSG:
handleLoginStatus(cmdMessage[1]);
break;
case LINK_MSG:
String linkMsg = cmdMessage[1];
handleLinkStatus(linkMsg);
break;
case DEVICE_REGISTRATION:
String code = cmdMessage[1];
JOptionPane.showMessageDialog(null, "Enter the following code on your device: " + code, "Set up Synchronization", JOptionPane.PLAIN_MESSAGE);
break;
case ALERT_MSG:
String alertMsg = cmdMessage[1];
logActivity("Alert: " + alertMsg);
if (!duplicateDialog(alertMsg)) {
JOptionPane.showMessageDialog(null, alertMsg, "Alert", JOptionPane.PLAIN_MESSAGE);
}
break;
case NOACCOUNT_MSG:
String noAcctMsg = cmdMessage[1];
logActivity("Alert: " + noAcctMsg);
JOptionPane.showMessageDialog(null, noAcctMsg, "No auction account", JOptionPane.PLAIN_MESSAGE);
break;
case NOTIFY_MSG:
String notifyMsg = cmdMessage[1];
logActivity("Notify: " + notifyMsg);
handleNotify(notifyMsg);
break;
case IGNORABLE_MSG:
String configstr = cmdMessage[1];
handleIgnorable(configstr);
break;
case ERROR_MSG:
String errorMsg = cmdMessage[1];
logActivity("Error: " + errorMsg);
JOptionPane.showMessageDialog(null, errorMsg, "An error occurred", JOptionPane.PLAIN_MESSAGE);
break;
case INVALID_LOGIN_MSG:
String rest = cmdMessage[1];
handleInvalidLogin(rest);
break;
case PRICE:
if (mFrame != null) {
mFrame.setPrice(cmdMessage[1]);
}
break;
default:
String msg = (String) deQ;
logActivity(msg);
setStatus(msg);
break;
}
}
/**
* Set the primary UI frame.
*
* @param frame The window (JFrame) that we are doing our primary display and UI for.
*/
public void setMainFrame(MacFriendlyFrame frame) {
mFrame = frame;
}
/**
* @param newStatus The text to place on the status line.
* @brief Sets the text in the status bar on the bottom of the screen.
*/
private void setStatus(String newStatus) {
mNow.setTime(System.currentTimeMillis());
String defaultServerTime = serverManager.getDefaultServerTime();
String bracketed = " [" + defaultServerTime + ']';
if (JConfig.queryConfiguration("timesync.enabled", "true").equals("false")) {
TimeZone tz = serverManager.getServer().getOfficialServerTimeZone();
if (tz != null && tz.hasSameRules(mCal.getTimeZone())) {
bracketed = " [" + Constants.localClockFormat.format(mNow) + ']';
}
}
String statusToDisplay = newStatus + bracketed;
if (mFrame != null) {
mFrame.setStatus(statusToDisplay);
} else {
JConfig.log().logDebug(newStatus + bracketed);
}
}
private void logActivity(String action) {
MQFactory.getConcrete("activity").enqueue(action);
}
private boolean duplicateDialog(String alertMsg) {
Window[] rval = JDialog.getWindows();
for (Window w : rval) {
if (w instanceof JDialog) {
JDialog jd = (JDialog)w;
if(jd.isVisible()) {
Component[] components = jd.getContentPane().getComponents();
for(Component c : components) {
if(c instanceof JOptionPane) {
if(((JOptionPane)c).getMessage().equals(alertMsg)) return true;
}
}
}
}
}
return false;
}
private void handleLoginStatus(String status) {
if(status.startsWith("FAILED")) {
toolBar.setToolTipText("Login failed.");
toolBar.setTextIcon(redStatus, redStatus16);
JConfig.getMetrics().trackEvent("login", "fail");
notifyAlert(status.substring("FAILED ".length()));
} else if(status.startsWith("CAPTCHA")) {
toolBar.setToolTipText("Login failed due to CAPTCHA.");
toolBar.setTextIcon(redStatus, redStatus16);
JConfig.getMetrics().trackEvent("login", "captcha");
} else if(status.startsWith("SUCCESSFUL")) {
String additionTooltip = "";
if(!status.equals("SUCCESSFUL")) {
String successMessage = status.substring("SUCCESSFUL ".length());
additionTooltip = "\n " + successMessage;
notifyAlert(successMessage);
}
toolBar.setToolTipText("Last login was successful." + additionTooltip);
toolBar.setTextIcon(greenStatus, greenStatus16);
JConfig.getMetrics().trackEvent("login", "success");
} else { // Status == NEUTRAL
toolBar.setToolTipText("Last login did not clearly fail, but no valid cookies were received.");
toolBar.setTextIcon(yellowStatus, yellowStatus16);
JConfig.getMetrics().trackEvent("login", "neutral");
}
}
private void handleValidLogin() {
MQFactory.getConcrete("Swing").enqueue("SNIPECHANGED");
auctionsManager.start();
searcher.start();
toolBar.setToolTipExtra(null);
_userValid = true;
}
private void startUpdating() {
MQFactory.getConcrete("Swing").enqueue("SNIPECHANGED");
auctionsManager.start();
searcher.start();
if (!_userValid) {
notifyAlert("Not yet logged in. Snipes will not fire until logging in\n" +
"is successful. Item updating has been enabled, but any\n" +
"features that rely on being logged in will not work.");
}
}
private void notifyAlert(String alertMessage) {
String msgType = ALERT_MSG;
String destination = "Swing";
if (Platform.isTrayEnabled()) {
msgType = NOTIFY_MSG;
destination = "tray";
}
MQFactory.getConcrete(destination).enqueue(msgType + alertMessage);
}
private void handleInvalidLogin(String rest) {
_userValid = false;
if (rest.length() != 0) {
// Eliminate a space that's there for readibility.
rest = rest.substring(1);
toolBar.setToolTipExtra(rest);
logActivity(rest);
} else {
logActivity("Invalid login.");
}
}
private void handleIgnorable(String configstr) {
int configLen = configstr.indexOf(' ');
String realMsg = configstr.substring(configLen + 1);
configstr = configstr.substring(0, configLen);
OptionUI oui = new OptionUI();
oui.promptWithCheckbox(null, realMsg, "Alert", configstr, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_OPTION);
}
private void handleNotify(String notifyMsg) {
if (Platform.isTrayEnabled()) {
MQFactory.getConcrete("tray").enqueue(NOTIFY_MSG + " " + notifyMsg);
} else {
MQFactory.getConcrete("Swing").enqueue(notifyMsg);
}
}
private void alterSnipeStatus() {
if (Platform.isTrayEnabled()) {
AuctionStats as = serverManager.getStats();
if (as != null) {
StringBuilder snipeText = new StringBuilder("TOOLTIP ");
if (as.getSnipes() != 0) {
snipeText.append("Next Snipe at: ").append(Constants.remoteClockFormat.format(as.getNextSnipe().getSnipeDate())).append('\n');
snipeText.append(as.getSnipes()).append(" snipes outstanding\n");
}
if (as.getCompleted() != 0) {
snipeText.append(as.getCompleted()).append(" auctions completed\n");
}
snipeText.append(as.getCount()).append(" auctions total");
MQFactory.getConcrete("tray").enqueue(snipeText.toString());
}
}
}
private void showUI() {
mFrame.setVisible(true);
if (Platform.isTrayEnabled()) {
MQFactory.getConcrete("tray").enqueue("RESTORED");
}
mFrame.setState(Frame.NORMAL);
}
private void hideUI() {
if (mFrame.isVisible()) {
UISnapshot.recordLocation(mFrame);
}
mFrame.setVisible(false);
if (Platform.isTrayEnabled()) {
MQFactory.getConcrete("tray").enqueue("HIDDEN");
}
}
private void toggleVisibility() {
mFrame.setVisible(!mFrame.isVisible());
MQFactory.getConcrete("tray").enqueue(mFrame.isVisible() ? "RESTORED" : "HIDDEN");
if (mFrame.isVisible()) mFrame.setState(Frame.NORMAL);
}
private void handleLinkStatus(String linkStat) {
setLinkUp(linkStat.startsWith("UP"));
String rest = linkStat.substring(linkStat.startsWith("UP") ? 2 : 4);
if (rest.length() == 0) {
if (_userValid) toolBar.setToolTipExtra(null);
} else {
// Skip a 'space' at the start.
rest = rest.substring(1);
logActivity("Link issues:");
logActivity(rest);
if (_userValid) toolBar.setToolTipExtra(rest);
}
}
private void handleHeader(String headerMsg) {
toolBar.setText(headerMsg);
}
private static final int ONEK = 1024;
private static final int UPDATE_FRAME_WIDTH = 640;
private static final int UPDATE_FRAME_HEIGHT = 450;
/**
* @brief Announce that a new version is available, and let the user
* decide what to do about it.
*/
private static void announceNewVersion() {
List<String> buttons = new ArrayList<>();
buttons.add("Download");
buttons.add("Ignore");
final UpdaterEntry ue = UpdateManager.getInstance().getUpdateInfo();
StringBuffer fullMsg = new StringBuffer(4 * ONEK);
String icon = JConfig.getResource("/jbidwatch64.jpg").toString();
fullMsg.append("<html><body><table><tr><td><img src=\"").append(icon).append("\"></td>");
fullMsg.append("<td valign=\"top\"><span class=\"banner\"><b>A new version of " + Constants.PROGRAM_NAME + " is available!</b></span><br>");
fullMsg.append("<span class=\"smaller\">" + Constants.PROGRAM_NAME + " <b>").append(ue.getVersion());
fullMsg.append("</b> is now available. Would you like to <a href=\"").append(ue.getURL()).append("\">download it now?</a><br><br>");
fullMsg.append("Upgrading is <em>").append(ue.getSeverity()).append("</em></span></td></tr></table>");
fullMsg.append("<p><b>Release Notes:</b></p><div class=\"changelog\">");
String changelog = ue.getChangelog();
if(changelog == null) {
fullMsg.append(ue.getDescription());
} else {
fullMsg.append(changelog);
}
fullMsg.append("</div></body></html>");
MyActionListener mal = new MyActionListener() {
private final String go_to = ue.getURL();
/**
* Handle the user's actions, in the dialog.
*
* @param listen_ae What the user did (clicked on) in the dialog.
*/
public void actionPerformed(ActionEvent listen_ae) {
String actionString = listen_ae.getActionCommand();
if (actionString.equals("Download")) {
MQFactory.getConcrete("browse").enqueue(go_to);
}
m_within.dispose();
m_within = null;
}
};
OptionUI oui = new OptionUI();
JFrame newFrame = oui.showChoiceTextDisplay(new JHTMLOutput("Version " + ue.getVersion() + " available!", fullMsg).getStringBuffer(),
new Dimension(UPDATE_FRAME_WIDTH, UPDATE_FRAME_HEIGHT), "Version " + ue.getVersion() + " available!", buttons,
"Upgrade information", mal);
mal.setFrame(newFrame);
}
private static long lastTime;
/**
* @brief Show the time once a second, in strikeout if the link to
* the default auction server is down.
*/
private void checkClock() {
long now = System.currentTimeMillis();
if (lastTime != 0) {
if ((lastTime + Constants.ONE_MINUTE) < now) {
// We've been out for more than a minute!
handleSleepDeprivation(now - lastTime);
}
}
lastTime = now;
String defaultServerTime = serverManager.getDefaultServerTime();
if (JConfig.queryConfiguration("display.toolbar", "true").equals("true")) {
defaultServerTime = "<b>" + defaultServerTime.replace("@", "</b><br>");
}
if (!_userValid) defaultServerTime = "Not logged in...";
String headerLine = getLinkUp() ? defaultServerTime : "<strike>" + defaultServerTime + "</strike>";
if(mSmall) headerLine = "<small>" + headerLine + "</small>";
headerLine = "<html><body>" + headerLine + "</body></html>";
MQFactory.getConcrete("Swing").enqueue("HEADER " + headerLine);
}
private void handleSleepDeprivation(long delta) {
Date now = new Date();
String status = "We appear to be waking from sleep; networking may not be up yet.";
JConfig.log().logDebug(status);
JConfig.getMetrics().trackEventTimed("sleep", "sleep", (int)delta, true);
List<AuctionEntry> sniped = entryCorral.findAllSniped();
if (sniped != null && !sniped.isEmpty()) {
boolean foundSnipe = false;
for (AuctionEntry entry : sniped) {
entry.setLastStatus(status);
if (now.after(entry.getEndDate())) {
entry.setLastStatus("The computer may have slept through the snipe time!");
foundSnipe = true;
}
}
if (foundSnipe) {
status += " One or more snipes may not have been fired.";
JConfig.getMetrics().trackEvent("sleep", "snipe_missed");
}
MQFactory.getConcrete("Swing").enqueue(NOTIFY_MSG + status);
}
// Pause updates for 20 seconds
pauseManager.pause(20);
// In 25 seconds, log back in. This is because networking usually takes 15-20 seconds to restart after a sleep event.
AuctionServer mainServer = serverManager.getServer();
long wakeUp = System.currentTimeMillis() + (25 * Constants.ONE_SECOND);
AuctionQObject updateEvent = new AuctionQObject(AuctionQObject.MENU_CMD, AuctionServer.UPDATE_LOGIN_COOKIE, null);
SuperQueue.getInstance().getQueue().add(updateEvent, mainServer.getFriendlyName(), wakeUp);
}
}