package org.bbssh.session;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import net.rim.device.api.i18n.FieldPosition;
import net.rim.device.api.i18n.MessageFormat;
import net.rim.device.api.i18n.ResourceBundle;
import net.rim.device.api.system.SystemListener2;
import net.rim.device.api.ui.Screen;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.Status;
import net.rim.device.api.ui.container.PopupScreen;
import net.rim.device.api.util.IntHashtable;
import org.bbssh.BBSSHApp;
import org.bbssh.exceptions.FontNotFoundException;
import org.bbssh.i18n.BBSSHResource;
import org.bbssh.model.ConnectionProperties;
import org.bbssh.model.Key;
import org.bbssh.model.KeyManager;
import org.bbssh.net.session.SessionListener;
import org.bbssh.net.session.SshSession;
import org.bbssh.net.session.TelnetSession;
import org.bbssh.notifications.NotificationManager;
import org.bbssh.platform.PlatformServicesProvider;
import org.bbssh.terminal.TerminalStateData;
import org.bbssh.ui.components.KeyPasswordPrompt;
import org.bbssh.ui.screens.PasswordPromptPopup;
import org.bbssh.ui.screens.TerminalScreen;
import org.bbssh.util.Logger;
import org.bbssh.util.Tools;
import org.bbssh.util.Version;
/**
* This class manages active connections/sessions and their screens.
*/
public class SessionManager implements SystemListener2, SessionListener {
ResourceBundle bundle = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME);
String tempPass;
boolean forceShutdown = false;
private TerminalScreen terminal = null;
public RemoteSessionInstance activeSession;
private int nextSessionId = 0;
private IntHashtable sessions = new IntHashtable();
// Future expansion --- allows us to have multiple conneced session interfaces.
private Hashtable sessionsForProperties = new Hashtable();
private static SessionManager me;
private SessionManager() {
}
/**
* Retrieve the one and only session manager instance
*
* @return session manager instance
*/
public static SessionManager getInstance() {
// technically should be synchronized, but our very first call is
// executed safely -- and that's the one that will cause this initialization to
// occur. We'll save the overhead on getInstance which will be used frequently.
if (me == null) {
me = new SessionManager();
}
return me;
}
/**
* Sets the active session by updating the terminal. Pushes term screen onto the display stack if it's not there -
* but will not uncovers it if it's buried.
*
* @param session
*/
private synchronized void setActiveSessionImpl(RemoteSessionInstance session) {
TerminalScreen s = getTerminalScreen();
try {
s.attachSession(session);
} catch (FontNotFoundException e1) {
Logger.error("FontNotFoundException in SessionManager.setActiveSessionImpl [ " + e1.getMessage() + " ] ");
BBSSHApp.inst().invokeLater(new Runnable() {
public void run() {
Status.show(Tools.getStringResource(BBSSHResource.MSG_FONT_LOAD_FAILED));
}
});
return;
}
try {
if (s.isDisplayed()) {
} else {
UiApplication.getUiApplication().pushScreen(s);
}
} catch (Exception e) {
Logger.fatal(e.getMessage() + "/" + e.toString());
}
}
public synchronized void setActiveSession(final RemoteSessionInstance session) {
activeSession = session;
cancelNotifications(session);
if (UiApplication.getUiApplication().isEventThread()) {
setActiveSessionImpl(session);
} else {
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
setActiveSessionImpl(session);
}
});
}
}
/**
* Switches display to another active session
*
* @param switchTo
* session to switch to.
*/
public synchronized void setActiveSession(int switchTo) {
RemoteSessionInstance con = (RemoteSessionInstance) sessions.get(switchTo);
if (con == null) {
return;
}
setActiveSession(con);
}
/**
* Reconnects the provided session instance, if it is disconnected.
*
* @param inst
* a disconnected remote session instance
*/
public void reconnectSession(RemoteSessionInstance inst) {
if (inst.isConnected()) {
return; // can only reconnect disconnected
}
sessions.remove(inst.session.getSessionId());
Vector v = (Vector) sessionsForProperties.get(inst.session.getProperties());
v.removeElement(inst);
connectSession(inst.session.getProperties());
}
private synchronized RemoteSessionInstance initiateSession(ConnectionProperties prop) {
RemoteSessionInstance c = new RemoteSessionInstance();
c.state = new TerminalStateData(prop);
nextSessionId++;
// New connections have extra work ahead...
if (prop.getSessionType() == ConnectionProperties.SESSION_TYPE_SSH) {
c.session = new SshSession(prop, nextSessionId, this);
} else {
c.session = new TelnetSession(prop, nextSessionId, this);
}
sessions.put(nextSessionId, c);
Vector v = (Vector) sessionsForProperties.get(prop);
if (v == null) {
v = new Vector();
sessionsForProperties.put(prop, v);
}
v.addElement(c);
return c;
}
/**
* Creates an active session for specified session properties, or resumes it if it is already active. If multiple
* sessions are available, it will look for a connected one. It will resume teh first connected session; if none are
* connected, it will resume the first session (even if disconnected)
*
* @param prop
* session properties representing the session to activate.
*/
public void initiateOrResumeSession(ConnectionProperties prop) {
RemoteSessionInstance rsi = getFirstConnectedSession(prop);
if (rsi == null) {
connectSession(initiateSession(prop));
} else {
setActiveSession(rsi);
}
Vector v = (Vector) sessionsForProperties.get(prop);
if (v == null || v.size() == 0) {
connectSession(prop);
} else {
setActiveSession((RemoteSessionInstance) v.elementAt(0));
}
}
public void connectSession(ConnectionProperties prop) {
connectSession(initiateSession(prop));
}
String tempUsername;
// @todo - this doesn't belong here. Who shoudl actually push the passowrd
// prompt
// @todo - redundant with the generic-ish ssh prompt handling. A way to
// consolidate?
private void updateCredentials() {
UiApplication.getUiApplication().invokeAndWait(new Runnable() {
public void run() {
PasswordPromptPopup prompt =
new PasswordPromptPopup(bundle.getString(BBSSHResource.SESSION_PROMPT_USERNAME_PASS),
tempUsername, false);
if (prompt.show()) {
tempUsername = prompt.getUsername();
tempPass = prompt.getPassword();
}
}
});
}
private void connectSession(RemoteSessionInstance c) {
if (c == null)
return;
c.emulator = c.session.getEmulator();
setActiveSession(c);
if (!c.isConnected()) {
// If password and username are blank; or password only is blank and
// no key is set,
// prompt for password.
final ConnectionProperties prop = c.session.getProperties();
tempUsername = prop.getUsername();
tempPass = null;
if (prop.getPassword() == null && (tempUsername == null || prop.getKeyId() == -1)) {
updateCredentials();
}
c.session.setUserName(tempUsername);
c.session.setPassword(tempPass);
c.session.connect();
}
refreshConnectionList();
}
public void disconnectSession(int sessionId) {
Logger.info("Disconnecting: " + sessionId);
disconnectSession((RemoteSessionInstance) sessions.get(sessionId));
}
private void disconnectSession(final RemoteSessionInstance rsi, final boolean terminate) {
if (rsi == null || rsi.session == null) {
Logger.warn("Requested to disconnect session, but no session provided.");
return;
}
try {
rsi.state.suppressNotify = terminate;
rsi.session.disconnect();
} finally {
// @todo - this shoudl be done in a background thread? Only a
// concern if we ever have to blcok for a
// significant period of time on our connectio mutex.
if (terminate) {
Vector v = (Vector) sessionsForProperties.get(rsi.session.getProperties());
v.removeElement(rsi);
sessions.remove(rsi.session.getSessionId());
if (activeSession == rsi) {
Logger.warn("Terminating active session.");
activeSession = null;
if (terminal != null && terminal.isDisplayed()) {
Logger.warn("Forcing terminate pop.");
UiApplication.getUiApplication().popScreen(terminal);
}
}
}
refreshConnectionList();
}
}
/**
* Begins the disconnect process in a background thread.
*
* @param rsi
*/
public void disconnectSession(final RemoteSessionInstance rsi) {
disconnectSession(rsi, false);
}
/**
* Iterates through all managed sessions and returns the total which are currently connected.
*
* @return number of connected sessions
*/
public boolean doesActiveSessionExist() {
Enumeration e = sessions.elements();
while (e.hasMoreElements()) {
RemoteSessionInstance rsi = (RemoteSessionInstance) e.nextElement();
if (rsi.isConnected()) {
return true;
}
}
return false;
}
/**
* @return Enumeration of RemoteSessionInstance
*/
public Enumeration getAvailableSessions() {
return sessions.elements();
}
public RemoteSessionInstance getSession(int sessionId) {
return (RemoteSessionInstance) sessions.get(sessionId);
}
public void terminateAllSessions() {
Enumeration e = sessions.elements();
forceShutdown = true;
while (e.hasMoreElements()) {
((RemoteSessionInstance) e.nextElement()).session.disconnect();
}
}
public void refreshConnectionList() {
BBSSHApp.inst().getPrimaryScreen().refreshConnections();
}
public void notifyAppDeactivate() {
refreshNotifications();
}
public synchronized TerminalScreen getTerminalScreen() {
if (terminal == null) {
String name = TerminalScreen.class.getName();
terminal = (TerminalScreen) Version.createOSObjectInstance(name);
;
}
return terminal;
}
// @todo does this listener belong in notificationmanager?
public void backlightStateChange(boolean on) {
if (on) {
notifyAppActivate();
} else {
notifyAppDeactivate();
}
}
public void cradleMismatch(boolean mismatch) {
}
public void fastReset() {
terminateAllSessions();
}
public void powerOffRequested(int reason) {
}
public void usbConnectionStateChange(int state) {
}
public void batteryGood() {
}
public void batteryLow() {
}
public void batteryStatusChange(int status) {
}
public void powerOff() {
}
public void powerUp() {
}
/**
* For a given session, this will close it (if necessary) then remove it compeltely - rendering it inaccessible the
* user.
*
* @param sessionId
*/
public void terminateSession(int sessionId) {
terminateSession(getSession(sessionId));
}
public void terminateSession(final RemoteSessionInstance rsi) {
if (rsi != null && rsi.session != null) {
disconnectSession(rsi, true);
}
}
public RemoteSessionInstance getFirstConnectedSession(ConnectionProperties prop) {
Vector v = getSessions(prop);
int size;
if (v == null || ((size = v.size()) == 0)) {
return null;
}
RemoteSessionInstance rsi = null;
RemoteSessionInstance ret = null;
for (int x = 0; x < size; x++) {
rsi = ((RemoteSessionInstance) v.elementAt(x));
if (rsi != null && rsi.session != null && rsi.session.isConnectionActive()) {
ret = rsi;
break;
}
}
return ret;
}
public Vector getSessions(ConnectionProperties prop) {
Object o = sessionsForProperties.get(prop);
Vector v;
if (o instanceof Vector) {
v = (Vector) o;
} else {
v = new Vector();
sessionsForProperties.put(prop, v);
}
return (Vector) o;
}
public RemoteSessionInstance getFirstSession(ConnectionProperties prop) {
if (prop == null)
return null;
Vector v = getSessions(prop);
if (v == null || v.size() == 0)
return null;
return (RemoteSessionInstance) v.elementAt(0);
}
public void notifyActiveSessionExposed() {
cancelNotifications(activeSession);
if (activeSession != null) {
PlatformServicesProvider.getInstance().lockOrientation(activeSession.state.orientationMode);
// @todo as this is key mapping it may belong with the other
onDisplayInvalid(activeSession.session.getSessionId());
}
}
public void notifyActiveSessionObscured() {
}
public void notifySessionListExposed() {
// When the session list is exposed.. we may be able to remove this.
}
public void cancelNotifications(RemoteSessionInstance rsi) {
if (rsi != null && rsi.state.notified) {
rsi.state.notified = false;
}
NotificationManager.inst().updateNotificationIndicators(false, rsi);
}
private String getKeyPasswordInternal(Key key) {
String password = null;
KeyPasswordPrompt passwordScreen = new KeyPasswordPrompt();
if (passwordScreen.doModal(key.getFriendlyName())) {
password = passwordScreen.getPassword();
if (password.length() == 0) {
password = null;
} else {
}
if (passwordScreen.getSavePassword()) {
key.setPassphrase(password);
KeyManager.getInstance().commitData();
}
}
return password;
}
public String getKeyPassword(int sessionId, Key key) {
// @todo - make a separate KeyPasswordRequestor implementation, much
// cleaner than this foolishness.
final Key k = key;
UiApplication.getUiApplication().invokeAndWait(new Runnable() {
public void run() {
tempPass = getKeyPasswordInternal(k);
}
});
String local = tempPass;
tempPass = null; // make sure this password can get freed up- (? do
// strings get freed in the jvm?)
return local;
}
public void onDisplayDirty(int sessionId) {
if (forceShutdown)
return;
terminal.redraw(false);
}
public void onDisplayInvalid(int sessionId) {
if (forceShutdown)
return;
terminal.redraw(true);
}
// onSessionError then, onSessionDisconnected
public void onSessionConnected(int sessionId) {
if (forceShutdown)
return;
// This will update home screen icons, notification icons, etc.
setSessionNotifiedState(sessionId);
}
public void onSessionDisconnected(int sessionId, int bytesWritten, int bytesRead) {
if (forceShutdown)
return;
RemoteSessionInstance rsi = (RemoteSessionInstance) sessions.get(sessionId);
if (rsi == null)
return;
if (rsi == activeSession) {
terminal.hideOverlayManager();
}
MessageFormat mf = new MessageFormat(bundle.getString(BBSSHResource.MSG_USAGE_SUMMARY));
StringBuffer sb = new StringBuffer();
mf.format(new Object[] {
Tools.byteCountToHumanReadableString(bytesRead), Tools.byteCountToHumanReadableString(bytesWritten),
Tools.byteCountToHumanReadableString(bytesRead + bytesWritten)
}, sb, new FieldPosition(0));
String value = sb.toString();
Logger.warn(value);
// Moving the bottom margin to the bottom of the screen will ensure that scrolling is permitted -- so when we
// append the message, the screen will be able to scroll as necessary.
rsi.emulator.setBottomMargin(rsi.emulator.getTerminalHeight(), false);
rsi.emulator.putStringStartLine(value);
// ANy time we force text into the terminal, we need to invalidate it to
// force repaint.
onDisplayInvalid(sessionId);
if (isTerminalActive()) {
if (rsi == activeSession) {
terminal.showExpiringMessage(bundle.getString(BBSSHResource.TERMINAL_MSG_DISCONNECTED));
} else {
terminal.showExpiringMessage(MessageFormat.format(
bundle.getString(BBSSHResource.TERMINAL_MSG_DISCONNECTED_OTHER), new Object[] {
rsi.session.getProperties()
}));
}
}
setSessionNotifiedState(sessionId);
}
/*
* Invoked when asession error causes the connection to be terminated.
* Received before onSessionDisconnected. (non-Javadoc)
*
* @see org.bbssh.net.session.SessionListener#onSessionError(int,
* java.lang.String)
*/
public void onSessionError(int sessionId, String errorMessage) {
if (forceShutdown)
return;
// rsi.state.addNotificationMessage("Error: " + error);
RemoteSessionInstance rsi = getSession(sessionId);
if (rsi == null)
return;
final String error = bundle.getString(BBSSHResource.MSG_NOTICE) + errorMessage;
rsi.state.error = true;
rsi.emulator.setBottomMargin(rsi.state.numRows, false);
rsi.emulator.putStringStartLine(Tools.CRLF);
rsi.emulator.putStringStartLine(error);
rsi.emulator.putStringStartLine(Tools.CRLF);
onDisplayInvalid(sessionId);
setSessionNotifiedState(sessionId);
}
/**
* @return true if the terminal screen is currently displayed foremost on screen.
*/
public boolean isTerminalActive() {
BBSSHApp app = BBSSHApp.inst();
if (terminal == null)
return false;
if (!app.isForeground())
return false;
Screen s = app.getActiveScreen();
if (s == terminal)
return true;
if (terminal.isVisible())
return true;
// if a popup screen is on top of the terminal (such as macro
// selector or font dialog)
// we'll count that as terminal active.
if (terminal.isDisplayed() && s instanceof PopupScreen) {
return true;
}
return false;
}
public void refreshNotifications() {
NotificationManager.inst().updateNotificationIndicators(false, null);
}
public void setSessionNotifiedState(int sessionId) {
if (forceShutdown)
return;
RemoteSessionInstance rsi = getSession(sessionId);
if (rsi == null) {
return;
}
if (!isTerminalActive()) {
boolean newNotification = !rsi.state.notified;
rsi.state.notified = true;
NotificationManager.inst().updateNotificationIndicators(newNotification, rsi);
// @todo - should NotificationManager also handle primaryscreen updates?
}
refreshConnectionList();
}
/**
* notify of a bell tone
*
* @param connId
*/
public void onSessionRemoteAlert(int connId) {
if (forceShutdown)
return;
RemoteSessionInstance rsi = getSession(connId);
if (rsi == null) {
return;
}
if (isTerminalActive()) {
if (rsi != activeSession) {
terminal.showExpiringMessage(MessageFormat.format(
bundle.getString(BBSSHResource.TERMINAL_MSG_ALERT_IN_SESSION), new Object[] {
rsi.session.getProperties()
}));
}
}
setSessionNotifiedState(connId);
}
public ConnectionProperties getPropertiesForInstance(int sessionId) {
RemoteSessionInstance rsi = getSession(sessionId);
if (rsi == null)
return null;
if (rsi.session == null)
return null;
return rsi.session.getProperties();
}
/**
* Invoked when the application has been activated, eg swappedi n from teh background, or when we receive backlight
* on event.
*/
public void notifyAppActivate() {
// stop any blinking/alert notification behavior.
NotificationManager.inst().resetNotificationState();
// Restore proper orientation lock (as much as we can - a bug prevents
// full restoration
// in that if the user changed orientation, locking it won't set it back
// to what we want.)
if (isTerminalActive()) {
notifyActiveSessionExposed();
} else {
// Cancel active Notifications, but do not change icon state.
cancelNotifications(null);
PlatformServicesProvider.getInstance().unlockOrientation();
}
refreshNotifications();
refreshConnectionList();
}
}