package eu.hgross.blaubot.core; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import eu.hgross.blaubot.core.statemachine.IBlaubotConnectionStateMachineListener; import eu.hgross.blaubot.core.statemachine.states.FreeState; import eu.hgross.blaubot.core.statemachine.states.IBlaubotState; import eu.hgross.blaubot.core.statemachine.states.IBlaubotSubordinatedState; import eu.hgross.blaubot.core.statemachine.states.KingState; import eu.hgross.blaubot.core.statemachine.states.PeasantState; import eu.hgross.blaubot.core.statemachine.states.PrinceState; import eu.hgross.blaubot.core.statemachine.states.StoppedState; import eu.hgross.blaubot.admin.AbstractAdminMessage; import eu.hgross.blaubot.admin.CensusMessage; import eu.hgross.blaubot.messaging.IBlaubotAdminMessageListener; import eu.hgross.blaubot.util.Log; import eu.hgross.blaubot.util.Util; /** * Listens for {@link eu.hgross.blaubot.admin.CensusMessage}s, calculates the diff (left or joined * devices and prince changes) and communicates them through the {@link eu.hgross.blaubot.core.ILifecycleListener}s attached to this {@link eu.hgross.blaubot.core.Blaubot} instance. * * If attached to a {@link eu.hgross.blaubot.core.statemachine.ConnectionStateMachine}, dispatches the corresponding * events to it's listeners when a kingdom merge takes place, the king dies, the prince takes over and so on. */ public class LifeCycleEventDispatcher implements IBlaubotAdminMessageListener, IBlaubotConnectionStateMachineListener { public static final String LOG_TAG = "LifeCycleEventDispatchingListener"; /** * The listeners */ private final CopyOnWriteArrayList<ILifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>(); /** * maps the last census message for different networks by the king's * uniqueId (kingUniqueId -> lastCensusMessage) */ private final Map<String, CensusMessage> lastCensusMessages = new ConcurrentHashMap<>(); private final IBlaubotDevice ownDevice; /** * The last known king's uniqueDeviceId. * ONLY used to trigger onKingDeviceChanged in certain cases (onDisconnected() and if we have no * former census message. Is reset on notifyDisconnectedFromNetwork() calls. */ private String lastKnownKingUniqueDeviceId; /** * The last known prince device id. * ONLY used to trigger onPrinceDeviceChanged before onDisconnect() calls - not for any other * processing. */ private String lastKnownPrinceUniqueDeviceId; public LifeCycleEventDispatcher(IBlaubotDevice ownDevice) { this.ownDevice = ownDevice; } @Override public void onAdminMessage(AbstractAdminMessage adminMessage) { if (adminMessage instanceof CensusMessage) { CensusMessage cm = (CensusMessage) adminMessage; String currentNetworkKingUniqueId = cm.extractKingUniqueId(); boolean hasFormerCensusMessage = lastCensusMessages.containsKey(currentNetworkKingUniqueId); CensusMessage lastCensusMessage = hasFormerCensusMessage ? lastCensusMessages.get(currentNetworkKingUniqueId) : new CensusMessage(new HashMap<String, State>()); // create a set containing all new uniqueIds in the network Set<String> newUniqueIds = new HashSet<>(cm.getDeviceStates().keySet()); newUniqueIds.removeAll(lastCensusMessage.getDeviceStates().keySet()); // create a set containing all removed uniqueIds since the last // census message Set<String> missingUniqueIds = new HashSet<>(lastCensusMessage.getDeviceStates().keySet()); missingUniqueIds.removeAll(cm.getDeviceStates().keySet()); // check if the prince has changed String oldPrince = lastCensusMessage.extractPrinceUniqueId(); String newPrince = cm.extractPrinceUniqueId(); boolean princeChanged = (oldPrince == null && newPrince != null) || (newPrince == null && oldPrince != null) || (!(oldPrince == null && newPrince == null) && !newPrince.equals(oldPrince)); String oldKing = lastCensusMessage.extractKingUniqueId(); oldKing = oldKing == null ? lastKnownKingUniqueDeviceId : oldKing; String newKing = cm.extractKingUniqueId(); boolean kingChanged = (oldKing == null && newKing != null) || (newKing == null && oldKing != null) || (!(oldKing == null && newKing == null) && !newKing.equals(oldKing)); // call the listeners for each joined/left device or prince change but ignore our own device id final String ownDeviceId = this.ownDevice.getUniqueDeviceID(); newUniqueIds.remove(ownDeviceId); missingUniqueIds.remove(ownDeviceId); for (ILifecycleListener listener : lifecycleListeners) { // joined devices for (String uniqueId : newUniqueIds) { IBlaubotDevice device = new BlaubotDevice(uniqueId); listener.onDeviceJoined(device); } // king if (kingChanged) { IBlaubotDevice oldKingD = null, newKingD = null; if (oldKing != null) { oldKingD = new BlaubotDevice(oldKing); } if (newKing != null) { newKingD = new BlaubotDevice(newKing); } listener.onKingDeviceChanged(oldKingD, newKingD); } // prince if (princeChanged) { IBlaubotDevice oldPrinceD = null, newPrinceD = null; if (oldPrince != null) { oldPrinceD = new BlaubotDevice(oldPrince); } if (newPrince != null) { newPrinceD = new BlaubotDevice(newPrince); } listener.onPrinceDeviceChanged(oldPrinceD, newPrinceD); } // left devices for (String uniqueId : missingUniqueIds) { IBlaubotDevice device = new BlaubotDevice(uniqueId); listener.onDeviceLeft(device); } } lastCensusMessages.put(currentNetworkKingUniqueId, cm); lastKnownKingUniqueDeviceId = newKing; lastKnownPrinceUniqueDeviceId = newPrince; } } @Override public void onStateChanged(IBlaubotState oldState, IBlaubotState newState) { if (newState instanceof PeasantState) { PeasantState ps = (PeasantState) newState; if (ps.getConnectionAccomplishmentType().equals(PeasantState.ConnectionAccomplishmentType.BOWED_DOWN)) { // we bowed down to a new kingdom, so we have to disconnect from the current network // - clear the last census message from the old kingdom // -> for all device in the last census, call onDeviceLeft final String oldKingUniqueId; if (oldState instanceof IBlaubotSubordinatedState) { oldKingUniqueId = ((IBlaubotSubordinatedState) oldState).getKingUniqueId(); } else { // -- if no subordinate and bowing down, we had to be part of a network and the only part of a network who is not a subordinate, is the king oldKingUniqueId = this.ownDevice.getUniqueDeviceID(); } CensusMessage oldKingdomMsg = lastCensusMessages.remove(oldKingUniqueId); // lastKnownKingUniqueDeviceId = ps.getKingConnection().getRemoteDevice().getUniqueDeviceID(); if (oldKingdomMsg != null) { notfiyOnDeviceLeftForKingdom(oldKingdomMsg); } } else if (ps.getConnectionAccomplishmentType().equals(PeasantState.ConnectionAccomplishmentType.FOLLOWED_THE_HEIR_TO_THE_THRONE)) { // we connected to the prince after the king died // - treat the old kingdom's census message as the new kingdom's census to let the diff logic for the onJoin/onLeft do their magic on arrival of the next census from this kingdom // - clear the last census message from the old kingdom final IBlaubotSubordinatedState _oldState = (IBlaubotSubordinatedState) oldState; CensusMessage oldKingdomMsg = lastCensusMessages.remove(_oldState.getKingUniqueId()); // lastKnownKingUniqueDeviceId = ps.getKingConnection().getRemoteDevice().getUniqueDeviceID(); if (oldKingdomMsg != null) { lastCensusMessages.put(ps.getKingUniqueId(), oldKingdomMsg); } // the onLeft/onJoined events should follow by the arriving census messages } else if (!ps.getConnectionAccomplishmentType().equals(PeasantState.ConnectionAccomplishmentType.DEGRADATION)) { // if not a change from prince -> peasant inside the same network (degraded), notify that we connected to a new network notifyConnectedToNetwork(); // the onDeviceJoined will be triggered from the // CensusMessage // TODO: the order of events: onConnected() and // onDeviceJoined() is not guaranteed at the moment! (the // messaging could be faster) } } else if (newState instanceof FreeState) { // ignore stopped->free transitions for disconnected events if (!(oldState instanceof StoppedState)) { // -- we disconnected from a network final String kingUniqueDeviceIdFromState = Util.extractKingUniqueDeviceIdFromState(oldState, this.ownDevice); notifyDisconnectedFromNetwork(kingUniqueDeviceIdFromState); } } else if (newState instanceof KingState) { if (oldState instanceof FreeState) { // -- we changed to KingState from a FreeState (excludes the // case when we change to KingState from PrinceState) notifyConnectedToNetwork(); } else if (oldState instanceof PrinceState) { // -- we changed to KingState from a prince state -> we took the throne // - treat the old kingdom's census message as the new kingdom's census to let the diff logic for the onJoin/onLeft do their magic on arrival of the next census from this kingdom // - clear the last census message from the old kingdom final IBlaubotSubordinatedState _oldState = (IBlaubotSubordinatedState) oldState; CensusMessage oldKingdomMsg = lastCensusMessages.remove(_oldState.getKingUniqueId()); // lastKnownKingUniqueDeviceId = _oldState.getKingUniqueId(); if (oldKingdomMsg != null) { lastCensusMessages.put(this.ownDevice.getUniqueDeviceID(), oldKingdomMsg); } // the onLeft/onJoined events should follow by the arriving census messages } } else if (newState instanceof StoppedState) { // stop was called explicitly if (oldState instanceof KingState || oldState instanceof IBlaubotSubordinatedState) { // only notify, if we actually were part of a network (previous state was not FreeState and not StoppedState) final String kingUniqueDeviceIdFromState = Util.extractKingUniqueDeviceIdFromState(oldState, this.ownDevice); notifyDisconnectedFromNetwork(kingUniqueDeviceIdFromState); } // in any case forget all census messages lastCensusMessages.clear(); } } /** * Simply calls onConnected() on all registered listeners */ public void notifyConnectedToNetwork() { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Notifying lifecycle listeners onConnected() ..."); } for (ILifecycleListener listener : lifecycleListeners) { listener.onConnected(); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Done notifying onConnected()"); } } /** * Triggers onDeviceLeft(..) for all of the known devices from the old * network (except the own devices) followed by a onDisconnected() on * the {@link eu.hgross.blaubot.core.ILifecycleListener} added to this {@link eu.hgross.blaubot.core.Blaubot} * instance. * * @param oldKingUniqueId the uniqueDeviceId of the former network's king device. */ public void notifyDisconnectedFromNetwork(String oldKingUniqueId) { // -- the uniqueId of our old king is known as well as the new // king's uniqueId // as of // https://scm.mi.hs-rm.de/trac/2014maprojekt/2014maprojekt01/ticket/22 // we have to trigger onDeviceLeft for // each device of the former network (lastCensusMessage) followed by // onDisconnected() // then we have to call onConnected() and onDeviceJoined(device) for // each device of the new network // trigger onDeviceLeft for each of the former connected devices // except ourselves CensusMessage oldNetworksLastCensusMessage = lastCensusMessages.remove(oldKingUniqueId); if (oldNetworksLastCensusMessage != null) { notfiyOnDeviceLeftForKingdom(oldNetworksLastCensusMessage); } else { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "Never got a CensusMessage from my old network (King was: " + oldKingUniqueId + ")"); } } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Notifying lifecycle listeners onDisconnected()"); } // fire the princeChanged, kingChangend and onDisconnected IBlaubotDevice kDevice = new BlaubotDevice(oldKingUniqueId); IBlaubotDevice pDevice; if (oldNetworksLastCensusMessage != null && oldNetworksLastCensusMessage.extractPrinceUniqueId() != null) { pDevice = new BlaubotDevice(oldNetworksLastCensusMessage.extractPrinceUniqueId()); } else if (lastKnownPrinceUniqueDeviceId != null) { pDevice = new BlaubotDevice(lastKnownPrinceUniqueDeviceId); } else { pDevice = null; } for (ILifecycleListener listener : lifecycleListeners) { // trigger onPrinceDeviceChanged and onKingDeviceChanged with king and prince = null listener.onKingDeviceChanged(kDevice, null); listener.onPrinceDeviceChanged(pDevice, null); lastKnownKingUniqueDeviceId = null; lastKnownPrinceUniqueDeviceId = null; listener.onDisconnected(); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Done notifying onDisconnected()"); } } /** * Calls onDeviceLeft for each device in oldNetworksLastCensusMessage, that does not match our uniqueDeviceId * * @param oldNetworksLastCensusMessage the message to derive the onDeviceLeft calls from */ private void notfiyOnDeviceLeftForKingdom(CensusMessage oldNetworksLastCensusMessage) { // create the set of IBlaubotDevices to trigger a onDeviceLeft event for Set<IBlaubotDevice> leftDevices = new HashSet<>(); for (final String deviceUniqueId : oldNetworksLastCensusMessage.getDeviceStates().keySet()) { // check if we can ignore this device because it is one of // our own uniqueIds if (deviceUniqueId.equals(this.ownDevice.getUniqueDeviceID())) { continue; } // if we can't skip, add a blaubotDevice instance IBlaubotDevice device = new BlaubotDevice(deviceUniqueId); leftDevices.add(device); } // finally trigger the onDeviceLeft events for (ILifecycleListener listener : lifecycleListeners) { for (IBlaubotDevice leftDevice : leftDevices) { listener.onDeviceLeft(leftDevice); } } } @Override public void onStateMachineStopped() { // handled in onStateChange } @Override public void onStateMachineStarted() { // handled in onStateChange } /** * Adds an {@link eu.hgross.blaubot.core.ILifecycleListener} * * @param lifecycleListener the listener to add */ public void addLifecycleListener(ILifecycleListener lifecycleListener) { this.lifecycleListeners.add(lifecycleListener); } /** * Removes an {@link eu.hgross.blaubot.core.ILifecycleListener} * * @param lifecycleListener the listener to remove */ public void removeLifecycleListener(ILifecycleListener lifecycleListener) { this.lifecycleListeners.remove(lifecycleListener); } }