package org.limewire.core.impl.connection;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.connection.ConnectionItem;
import org.limewire.core.api.connection.ConnectionLifecycleEventType;
import org.limewire.core.api.connection.ConnectionStrength;
import org.limewire.core.api.connection.GnutellaConnectionManager;
import org.limewire.inject.EagerSingleton;
import org.limewire.lifecycle.Service;
import org.limewire.lifecycle.ServiceRegistry;
import org.limewire.listener.SwingSafePropertyChangeSupport;
import org.limewire.net.SocketsManager.ConnectType;
import org.limewire.util.Objects;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.limegroup.gnutella.ConnectionManager;
import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.connection.ConnectionLifecycleEvent;
import com.limegroup.gnutella.connection.ConnectionLifecycleListener;
import com.limegroup.gnutella.connection.RoutedConnection;
import com.limegroup.gnutella.util.LimeWireUtils;
/**
* An implementation of GnutellaConnectionManager for the live core.
*/
@EagerSingleton
public class GnutellaConnectionManagerImpl
implements GnutellaConnectionManager, ConnectionLifecycleListener {
/** The number of messages a connection must have sent before we consider it stable. */
private static final int STABLE_THRESHOLD = 5;
private final ConnectionManager connectionManager;
private final PropertyChangeSupport changeSupport = new SwingSafePropertyChangeSupport(this);
private final ConnectionServices connectionServices;
/** Mapping of connections to ConnectionItem instances. */
private final Map<RoutedConnection, ConnectionItem> connectionMap;
/** List of ConnectionItem instances. */
private final EventList<ConnectionItem> connectionItemList;
private volatile ConnectionStrength currentStrength = ConnectionStrength.DISCONNECTED;
volatile ConnectionLifecycleEventType lastStrengthRelatedEvent;
volatile long lastIdleTime;
/**
* Constructs the live implementation of GnutellaConnectionManager using
* the specified connection and library services.
*/
@Inject
public GnutellaConnectionManagerImpl(
ConnectionManager connectionManager,
ConnectionServices connectionServices) {
this.connectionManager = Objects.nonNull(connectionManager, "connectionManager");
this.connectionServices = connectionServices;
// Create map of connection items.
connectionMap = new HashMap<RoutedConnection, ConnectionItem>();
// Create list of connection items as thread safe list.
connectionItemList = GlazedListsFactory.threadSafeList(
new BasicEventList<ConnectionItem>());
}
/**
* Adds this as a listener for core connection events.
*/
@Inject
void registerListener() {
connectionManager.addEventListener(this);
}
/**
* Register the periodic connection strength updater service.
*/
@Inject
void registerService(ServiceRegistry registry, final @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor) {
registry.register(new Service() {
private volatile ScheduledFuture<?> meter;
private volatile ConnectionLifecycleListener listener;
@Override
public String getServiceName() {
return "Connection Strength Meter";
}
@Override
public void initialize() {
listener = new ConnectionLifecycleListener() {
@Override
public void handleConnectionLifecycleEvent(ConnectionLifecycleEvent evt) {
switch(evt.getType()) {
case NO_INTERNET:
case CONNECTION_INITIALIZED:
case CONNECTED:
lastStrengthRelatedEvent = evt.getType();
break;
}
}
};
connectionManager.addEventListener(listener);
}
@Override
public void start() {
meter = backgroundExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
setConnectionStrength(calculateStrength());
}
}, 0, 1, TimeUnit.SECONDS);
}
@Override
public void stop() {
if(meter != null) {
meter.cancel(false);
meter = null;
}
if(listener != null) {
connectionManager.removeEventListener(listener);
listener = null;
}
}
});
}
private void setConnectionStrength(ConnectionStrength newStrength) {
ConnectionStrength oldStrength = currentStrength;
currentStrength = newStrength;
changeSupport.firePropertyChange(GnutellaConnectionManager.CONNECTION_STRENGTH, oldStrength, newStrength);
}
ConnectionStrength calculateStrength() {
int stable = connectionManager.countConnectionsWithNMessages(STABLE_THRESHOLD);
ConnectionStrength strength;
if(stable == 0) {
int initializing = connectionManager.getNumFetchingConnections();
int connections = connectionManager.getNumInitializedConnections();
// No initializing or stable connections
if(initializing == 0 && connections == 0) {
//Not attempting to connect at all...
if(!connectionManager.isConnecting()) {
strength = ConnectionStrength.DISCONNECTED;
} else {
//Attempting to connect...
strength = ConnectionStrength.CONNECTING;
}
} else if(connections == 0) {
// No initialized, all initializing - connecting
strength = ConnectionStrength.CONNECTING;
} else {
// Some initialized - poor connection.
strength = ConnectionStrength.WEAK;
}
} else if(connectionManager.isConnectionIdle()) {
lastIdleTime = System.currentTimeMillis();
strength = ConnectionStrength.FULL;
} else {
int preferred = connectionManager.getPreferredConnectionCount();
// account for pro having more connections.
if(LimeWireUtils.isPro()) {
preferred -= 2;
}
// ultrapeers don't need as many...
if(connectionManager.isSupernode()) {
preferred -= 5;
}
preferred = Math.max(1, preferred); // prevent div by 0
double percent = (double)stable / (double)preferred;
if(percent <= 0.15) {
strength = ConnectionStrength.WEAK;
} else if(percent <= 0.30) {
strength = ConnectionStrength.WEAK_PLUS;
} else if(percent <= 0.5) {
strength = ConnectionStrength.MEDIUM;
} else if(percent <= 0.75) {
strength = ConnectionStrength.MEDIUM_PLUS;
} else if(percent <= 1) {
strength = ConnectionStrength.FULL;
} else /* if(percent > 1) */ {
strength = ConnectionStrength.TURBO;
}
}
switch(strength) {
case DISCONNECTED:
case CONNECTING:
if(lastStrengthRelatedEvent == ConnectionLifecycleEventType.NO_INTERNET) {
strength = ConnectionStrength.NO_INTERNET;
}
}
switch(strength) {
case CONNECTING:
case WEAK:
// if one of these four, see if we recently woke up from
// idle, and if so, report as 'waking up' instead.
long now = System.currentTimeMillis();
if(now < lastIdleTime + 15 * 1000)
strength = ConnectionStrength.MEDIUM;
}
return strength;
}
@Override
public boolean isConnected() {
return connectionServices.isConnected();
}
@Override
public boolean isUltrapeer() {
return connectionManager.isSupernode();
}
@Override
public void connect() {
connectionServices.connect();
}
@Override
public void disconnect() {
connectionServices.disconnect();
}
@Override
public void restart() {
connectionManager.disconnect(true);
connectionManager.connect();
}
@Override
public ConnectionStrength getConnectionStrength() {
return currentStrength;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* Returns the list of connections. An application should NOT assume that
* the returned list is Swing-compatible; Swing is suppported by wrapping
* the resulting list via a call to <code>
* GlazedListsFactory.swingThreadProxyEventList()</code>.
*/
@Override
public EventList<ConnectionItem> getConnectionList() {
return connectionItemList;
}
/**
* Removes the specified connection from the list.
*/
@Override
public void removeConnection(ConnectionItem item) {
if (item instanceof CoreConnectionItem) {
RoutedConnection connection = ((CoreConnectionItem) item).getRoutedConnection();
connectionServices.removeConnection(connection);
}
}
/**
* Attempts to establish a connection to the specified host and port.
*/
@Override
public void tryConnection(String hostname, int portnum, boolean useTLS) {
connectionServices.connectToHostAsynchronously(hostname, portnum,
useTLS ? ConnectType.TLS : ConnectType.PLAIN);
}
/**
* Handles connection lifecycle events fired by the ConnectionManager.
*/
@Override
public void handleConnectionLifecycleEvent(ConnectionLifecycleEvent evt) {
// Get connection.
RoutedConnection c = evt.getConnection();
if (c != null) {
if (evt.isConnectionInitializingEvent()) {
// Add new connection.
addConnection(c);
} else if (evt.isConnectionInitializedEvent()) {
// Update status when connection is fully initialized.
updateConnection(c);
} else if (evt.isConnectionClosedEvent()) {
// Remove connection when closed.
removeConnection(c);
}
}
}
/**
* Adds the specified connection to the list.
*/
private void addConnection(RoutedConnection connection) {
ConnectionItem connectionItem = connectionMap.get(connection);
if (connectionItem == null) {
connectionItem = new CoreConnectionItem(connection);
connectionMap.put(connection, connectionItem);
connectionItemList.add(connectionItem);
}
}
/**
* Updates the specified connection in the list.
*/
private void updateConnection(RoutedConnection connection) {
ConnectionItem connectionItem = connectionMap.get(connection);
if (connectionItem != null) {
connectionItem.update();
}
}
/**
* Removes the specified connection from the list.
*/
private void removeConnection(RoutedConnection connection) {
ConnectionItem connectionItem = connectionMap.get(connection);
if (connectionItem != null) {
connectionItemList.remove(connectionItem);
connectionMap.remove(connection);
}
}
}