com.lfv.lanzius.application.Controller.java Source code

Java tutorial

Introduction

Here is the source code for com.lfv.lanzius.application.Controller.java

Source

/** 
 * 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, see <http://www.gnu.org/licenses/>.
 */
package com.lfv.lanzius.application;

import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;

import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import com.arkatay.yada.base.ItemDispatcher;
import com.arkatay.yada.base.Time;
import com.arkatay.yada.codec.AudioDecoder;
import com.arkatay.yada.codec.AudioDecoderListener;
import com.arkatay.yada.codec.AudioEncoder;
import com.arkatay.yada.codec.AudioRecorder;
import com.arkatay.yada.codec.JSpeexDecoder;
import com.arkatay.yada.codec.JSpeexEncoder;
import com.arkatay.yada.codec.NullDecoder;
import com.arkatay.yada.codec.NullEncoder;
import com.lfv.lanzius.Config;
import com.lfv.lanzius.Constants;
import com.lfv.lanzius.DomTools;
import com.lfv.lanzius.application.full.FullView;
import com.lfv.lanzius.application.phoneonly.PhoneOnlyView;
import com.lfv.lanzius.application.slim.SlimView;
import com.lfv.lanzius.audio.MixingDataLine;
import com.lfv.yada.data.client.ClientBundle;
import com.lfv.yada.data.client.ClientChannel;
import com.lfv.yada.data.client.ClientTerminal;
import com.lfv.yada.net.Packet;
import com.lfv.yada.net.SocketAddress;
import com.lfv.yada.net.client.ClientNetworkHandler;
import com.lfv.yada.net.client.ClientNetworkManager;
import com.lfv.yada.net.client.ClientPacketReceiver;
import com.lfv.yada.net.client.Distributor;

public class Controller implements ViewEventHandler, ClientNetworkHandler, AudioDecoderListener, Constants {

    private Log log;
    private boolean autoTesterEnabled;
    private AutoTester autoTester;

    private AbstractView view;
    private JDialog settingsDialog;
    private JDialog isaDialog;
    private Document model;
    private ClientBundle bundle;

    private TerminalProperties properties;
    private SocketAddress serverSocketAddr;
    private ClientNetworkManager networkManager;
    private ClientPacketReceiver packetReceiver;

    private Timer timer;
    private Thread hookThread;

    private Mixer outputMixer;
    private Mixer inputMixer;

    // Codec n stuff
    private AudioEncoder encoder;
    private Distributor distributor;
    private AudioDecoder decoderPhone;
    private AudioDecoder decoderForward;

    // Monitor
    private boolean monitorActive;
    private int monitoringTerminalId;
    private int monitoredTerminalId;
    private int monitorChannelId;
    private AudioEncoder monitorEncoder;
    private Distributor monitorDistributor;

    // Recorder
    private AudioRecorder audioRecorder;

    // Sounds
    private SoundClip clipRingTone;
    private SoundClip clipRingBack;
    private SoundClip clipRingBusy;
    private SoundClip clipWarning;
    private SoundClip clipNotify;
    private boolean clipWarningOn;

    // Volume
    private List<VolumeAdjustable> volumeClipList;
    private List<VolumeAdjustable> volumeRadioList;

    // Control
    private int state;
    //private boolean restarting;
    private int reconnectPrintCount;

    private boolean semDisconnected;
    private boolean semChannelsLeft;
    private boolean semStartFinished;
    private boolean semStopFinished;

    private boolean phoneActive;
    private boolean radioEncoding;
    private boolean radioDecoding;
    private List<AudioDecoder> radioDecodingList;

    private int phoneState = PHONE_STATE_IDLE;
    private int radioState = RADIO_STATE_IDLE;
    private long radioIdleTime = System.currentTimeMillis();
    private List<ClientChannel> radioAcquiredChannelsList;
    private List<ClientChannel> radioUnacquiredChannelsList;
    private TimerTask radioReacquireTask;
    private int radioAcquireCount;
    private int radioReacquireCount;
    private int radioReleaseCount;

    private FootSwitchController footSwitch;
    private EventSwitchController eventSwitch;
    private PeripheralLink peripheralLink;

    private long isaReqStartTime;

    private static Controller controller = null;

    public static Controller getInstance() {
        if (controller == null)
            controller = new Controller();
        return controller;
    }

    private Controller() {
        log = LogFactory.getLog(getClass());
        log.info(Config.VERSION + "\n");
        timer = new Timer("Scontroller");
        radioAcquiredChannelsList = new LinkedList<ClientChannel>();
        radioUnacquiredChannelsList = new LinkedList<ClientChannel>();
        radioDecodingList = new LinkedList<AudioDecoder>();
        volumeClipList = new LinkedList<VolumeAdjustable>();
        volumeRadioList = new LinkedList<VolumeAdjustable>();
    }

    public void setAutoTester(boolean autoTesterEnabled) {
        this.autoTesterEnabled = autoTesterEnabled;
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) {
        }
    }

    /*
    private synchronized void restart() {
    if(state==CLIENT_STATE_STARTED && !restarting && timer!=null) {
        log.error("Restarting terminal!");
        timer.schedule(new TimerTask() {
            public void run() {
                stop(true,false);
            }
        }, DELAY_STOP);
        timer.schedule(new TimerTask() {
            public void run() {
                start();
                restarting = false;
            }
        }, DELAY_STOP+DELAY_STARTSTOP+DELAY_START);
        restarting = true;
    }
    }
    */

    private synchronized boolean waitChannelAcquiry() {
        try {

            // Wait until all channels are acquired, reacquired or released
            // This function must be called only from synchronized methods
            long t = System.currentTimeMillis();
            while ((radioAcquireCount > 0 || radioReacquireCount > 0 || radioReleaseCount > 0)
                    && (System.currentTimeMillis() < (t + TIMEOUT_ACQUIRY)))
                wait(100);

            // Failure
            if (radioAcquireCount > 0 || radioReacquireCount > 0 || radioReleaseCount > 0) {
                log.error("Wait for channel acquiry timed out! " + "(radioAcquireCount=" + radioAcquireCount + ","
                        + " radioReacquireCount=" + radioReacquireCount + "," + " radioReleaseCount="
                        + radioReleaseCount + ")");
            }

            // Success
            else {

                // Clean up and prepare for new requests
                radioAcquireCount = 0;
                radioReacquireCount = 0;
                radioReleaseCount = 0;

                radioUnacquiredChannelsList.clear();
                if (radioReacquireTask != null) {
                    radioReacquireTask.cancel();
                    radioReacquireTask = null;
                }

                return true;
            }
        } catch (InterruptedException ex) {
            log.warn("Wait for channel acquiry interrupted!");
        }

        log.error("!!!");
        log.error("!!! The terminal has entered an unstable state and may not function properly!");
        log.error("!!! Relink or restart the terminal if problems arise!");
        log.error("!!!");

        clear();

        return false;
    }

    private synchronized void releaseAcquiredChannels() {

        if (!waitChannelAcquiry())
            return;

        if (!Config.CLIENT_SERVERLESS) {

            // Reset only radio if phone is encoding, channels will
            // automatically be removed from the distributor
            if (phoneActive)
                encoder.resetChannels(distributor.getRadioChannels());

            // Stop encoder otherwise
            else
                encoder.stopProcessing();

            radioEncoding = false;
            updateMonitor();

            // Idle talk button
            model.getRootElement().getChild("TalkButton").setAttribute("state", "idle");

            // Release all acquired channels
            Iterator<ClientChannel> iter = radioAcquiredChannelsList.iterator();
            while (iter.hasNext()) {
                ClientChannel channel = iter.next();

                // Send release to server
                radioReleaseCount++;
                networkManager.radioRelease(channel.getId());

                // Update send flag
                channel.getElement().setAttribute("send", "false");
            }

            // Send message to peripheral link
            if (radioReleaseCount > 0 && peripheralLink != null) {
                peripheralLink.PostRadioSendStop();
            }

            // Clear all fail flags
            iter = bundle.getChannelCollection().iterator();
            while (iter.hasNext()) {
                ClientChannel channel = iter.next();
                if (channel.getId() > CHANNEL_RADIO_START) {
                    channel.getElement().setAttribute("fail", "false");
                }
            }
        } else if (peripheralLink != null) {
            // Serverless debug mode
            peripheralLink.PostRadioSendStop();
        }

        radioAcquiredChannelsList.clear();

        if (view != null)
            view.updateRadioView();
    }

    private synchronized List<Element> getAllPeerElements(int peerId) {
        List<Element> list = new LinkedList<Element>();
        Iterator iter1 = model.getRootElement().getChild("RoleSetup").getChildren().iterator();
        while (iter1.hasNext()) {
            Element er = (Element) iter1.next();
            Iterator iter2 = er.getChild("PhonePeers").getChildren().iterator();
            while (iter2.hasNext()) {
                Element err = (Element) iter2.next();
                if (DomTools.getAttributeInt(err, "id", 0, false) == peerId)
                    list.add(err);
            }
        }
        return list;
    }

    private synchronized Element getPeerElement(int roleId, int peerId) {
        Iterator iter1 = model.getRootElement().getChild("RoleSetup").getChildren().iterator();
        while (iter1.hasNext()) {
            Element er = (Element) iter1.next();
            if (DomTools.getAttributeInt(er, "id", 0, false) == roleId) {
                Iterator iter2 = er.getChild("PhonePeers").getChildren().iterator();
                while (iter2.hasNext()) {
                    Element err = (Element) iter2.next();
                    if (DomTools.getAttributeInt(err, "id", 0, false) == peerId)
                        return err;
                }
                return null;
            }
        }

        return null;
    }

    private synchronized String getRoleName(int id) {
        String def = "Withheld";
        String ids = String.valueOf(id & ID_MASK_ROLE);
        if (ids.equals("0"))
            log.warn("Get role name called with id 0");
        Iterator iter = model.getRootElement().getChild("RoleDefs").getChildren().iterator();
        while (iter.hasNext()) {
            Element er = (Element) iter.next();
            if (DomTools.getAttributeString(er, "id", "-", false).equals(ids)) {
                return DomTools.getChildText(er, "Name", def, false);
            }
        }
        return def;
    }

    private synchronized void updateLocalRoleActivity(int roleId, int peerId, boolean line, boolean blink) {
        Element ep = blink ? getPeerElement(roleId, peerId) : null;
        Iterator<Element> iter = getAllPeerElements(peerId).iterator();
        while (iter.hasNext()) {
            Element epi = iter.next();
            epi.setAttribute("line", line ? "3" : "1");
            epi.setAttribute("blink", String.valueOf(blink && (ep == epi)));
        }
        if (view != null)
            view.updatePhoneView();
    }

    private synchronized void updateMonitor() {

        // Return if monitor is disabled
        if ((monitorEncoder == null) || (monitorDistributor == null))
            return;

        // Is radio or phone active?
        boolean codecActive = (radioEncoding || radioDecoding || phoneActive);

        if (monitorActive) {
            // Stop if codec is not active or monitoring is turned off
            if (!codecActive || (monitoringTerminalId == 0) || (monitorChannelId == 0)) {
                monitorEncoder.stopProcessing();
                monitorActive = false;
            }
        } else {
            if (codecActive && (monitoringTerminalId > 0) && (monitorChannelId > 0)) {
                ClientTerminal monitoringTerminal = bundle.getTerminal(monitoringTerminalId);

                // Set up distributor and move to started state
                if (monitoringTerminal != null) {
                    monitorDistributor.setConfinedRecipient(monitoringTerminal, monitorChannelId);
                    monitorEncoder.startProcessing();
                    monitorActive = true;
                } else if (log.isDebugEnabled()) {
                    log.debug("Monitoring terminal is not available, ignoring");
                }
            }
        }
    }

    private synchronized void playSound(int soundId) {
        if (soundId == SOUND_RINGTONE)
            clipRingTone.play();
        else
            clipRingTone.stop();
        if (soundId == SOUND_RINGBACK)
            clipRingBack.play();
        else
            clipRingBack.stop();
        if (soundId == SOUND_RINGBUSY)
            clipRingBusy.play();
        else
            clipRingBusy.stop();
        if (soundId == SOUND_NOTIFY)
            clipNotify.play();
        else
            clipNotify.stop();
    }

    private synchronized void adjustVolumes() {

        Settings s = Settings.getInstance();

        float masterVolume = (float) s.getMasterVolume() / (float) Settings.DEF_MASTER_VOLUME;
        float rapassVolume = (float) s.getRapassVolume() / (float) (Settings.NBR_VOLUME_STEPS - 1);
        float chprioVolume = (float) s.getChprioVolume() / (float) (Settings.NBR_VOLUME_STEPS - 1);

        float phoneVolume = masterVolume;
        float forwardVolume = masterVolume * rapassVolume;
        float radioVolume = masterVolume;

        // Simultaneous radio and interphone
        if (radioDecoding && phoneActive) {
            int c = s.getChprioChoice();
            if (c == Settings.CHPRIO_PHONE) {
                radioVolume *= chprioVolume;
                log.debug("Simultaneous phone and radio, priority for phone. Reducing radio volume (" + radioVolume
                        + ")");
            } else if (c == Settings.CHPRIO_RADIO) {
                phoneVolume *= chprioVolume;
                forwardVolume *= chprioVolume;
                log.debug("Simultaneous phone and radio, priority for radio. Reducing phone volume (" + phoneVolume
                        + "/" + forwardVolume + ")");
            }
        }

        if (log.isDebugEnabled())
            log.debug("Adjusting volumes: " + radioVolume + ", " + phoneVolume + ", " + forwardVolume);

        if (decoderPhone != null)
            decoderPhone.setVolume(phoneVolume);
        if (decoderForward != null)
            decoderForward.setVolume(forwardVolume);

        Iterator<VolumeAdjustable> iter = volumeRadioList.iterator();
        while (iter.hasNext())
            iter.next().setVolume(radioVolume);
    }

    private synchronized void stopPhone() {
        packetReceiver.closeChannel(CHANNEL_PHONE);
        packetReceiver.closeChannel(CHANNEL_FORWARD);
        if (decoderPhone != null)
            decoderPhone.forceStop();
        if (decoderForward != null)
            decoderForward.forceStop();
        phoneActive = false;

        if (encoder != null) {
            // Reset only phone if radio is encoding, recipient will
            // automatically be removed from the distributor
            if (radioEncoding)
                encoder.resetChannel(CHANNEL_PHONE);

            // Stop encoder otherwise
            else
                encoder.stopProcessing();
        }

        adjustVolumes();
        updateMonitor();
    }

    private synchronized void clear() {

        semDisconnected = true;
        semChannelsLeft = true;
        semStartFinished = true;
        semStopFinished = true;

        phoneActive = false;
        radioEncoding = false;
        radioDecoding = false;
        radioDecodingList.clear();

        monitorActive = false;
        monitoringTerminalId = 0;
        monitoredTerminalId = 0;
        monitorChannelId = 0;

        phoneState = PHONE_STATE_IDLE;
        radioState = RADIO_STATE_IDLE;
        radioAcquiredChannelsList.clear();
        radioUnacquiredChannelsList.clear();
        radioAcquireCount = 0;
        radioReacquireCount = 0;
        radioReleaseCount = 0;

        footSwitch = null;
        networkManager = null;
        bundle = null;
        serverSocketAddr = null;

    }

    private synchronized boolean validateModel() {

        // Validate model
        if (model == null)
            return false;

        Element r = model.getRootElement();
        if (r == null)
            return false;

        if (!r.getName().equals("Terminal"))
            return false;
        if (r.getChild("Group") == null)
            return false;
        if (r.getChild("Codec") == null)
            return false;
        if (r.getChild("ChannelSetup") == null)
            return false;
        if (r.getChild("RoleSetup") == null)
            return false;
        if (r.getChild("RoleDefs") == null)
            return false;

        // Validate ChannelsSetup
        Iterator iter = r.getChild("ChannelSetup").getChildren().iterator();
        while (iter.hasNext()) {
            Element e = (Element) iter.next();
            // Check that child is expected
            if (!e.getName().equals("Channel"))
                return false;
            // Check mandatory id attribute
            if (e.getAttribute("id") == null)
                return false;
        }
        iter = r.getChild("RoleSetup").getChildren().iterator();
        while (iter.hasNext()) {
            Element e = (Element) iter.next();
            if (!e.getName().equals("Role"))
                return false;
            if (e.getChild("Name") == null)
                return false;
            if (e.getChild("PhonePeers") == null)
                return false;
            Iterator iter2 = e.getChild("PhonePeers").getChildren().iterator();
            while (iter2.hasNext()) {
                e = (Element) iter2.next();
                if (!e.getName().equals("RoleRef"))
                    return false;
                if (e.getChild("Name") == null)
                    return false;
            }
        }

        return true;
    }

    /*
    private synchronized void dbgPrintModelToFile(String name) {
    if(!Config.DEBUG) return;
    String fname = "data/development/client_model_"+name+".xml";
    XMLOutputter xmlo = new XMLOutputter(Format.getPrettyFormat());
    try {
        Writer wr = new FileWriter(fname);
        xmlo.output(model, wr);
        wr.close();
    } catch (IOException ex) {
        log.error("IOException when trying to write model to file "+fname+"!", ex);
    }
    log.info("Model successfully written to file "+fname);
    }
        
    private synchronized void dbgListThreadTree() {
    if(!Config.DEBUG) return;
    System.out.println("--------------------------------------------------");
    System.out.println("Current thread: "+Thread.currentThread());
    System.out.println("Active threads:");
    // Find the root thread group
    ThreadGroup root = Thread.currentThread().getThreadGroup().getParent();
    while (root.getParent() != null) {
        root = root.getParent();
    }
        
    // Visit each thread group
    dbgListThreadTreeRec(root, 0);
    System.out.println("--------------------------------------------------");
    }
        
    private synchronized void dbgListThreadTreeRec(ThreadGroup group, int level) {
    if(!Config.DEBUG) return;
    System.out.println(group);
        
    // Get threads in `group'
    int numThreads = group.activeCount();
    Thread[] threads = new Thread[numThreads*2];
    numThreads = group.enumerate(threads, false);
        
    // Enumerate each thread in `group'
    for (int i=0; i<numThreads; i++) {
        // Get thread
        Thread thread = threads[i];
        System.out.println("  "+thread);
    }
        
    // Get thread subgroups of `group'
    int numGroups = group.activeGroupCount();
    ThreadGroup[] groups = new ThreadGroup[numGroups*2];
    numGroups = group.enumerate(groups, false);
        
    // Recursively visit each subgroup
    for (int i=0; i<numGroups; i++) {
        dbgListThreadTreeRec(groups[i], level+1);
    }
    }
    */

    private synchronized void dbgListThreads() {
        if (!Config.DEBUG)
            return;
        System.out.println("--------------------------------------------------");
        System.out.println("Current thread: " + Thread.currentThread());
        System.out.println("Active main threads:");

        // Get threads in `group'
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        int numThreads = group.activeCount();
        Thread[] threads = new Thread[numThreads * 2];
        numThreads = group.enumerate(threads, false);

        // Enumerate each thread in `group'
        for (int i = 0; i < numThreads; i++) {
            // Get thread
            Thread thread = threads[i];
            System.out.println("  " + thread);
        }

        System.out.println("--------------------------------------------------");
    }

    public synchronized void init(int id) {

        clear();
        state = CLIENT_STATE_DISCONNECTED;
        reconnectPrintCount = 5;

        // Load terminal properties
        try {
            properties = new TerminalProperties(id);
        } catch (Exception ex) {
            log.error("Invalid or no terminal properties file (data/properties/terminalproperties.xml). Exiting!",
                    ex);
            System.exit(0);
        }

        // Overwrite if specified externally on the command line
        if (id > 0)
            properties.setTerminalId(id);

        log.info("Starting terminal " + properties.getTerminalId());

        // Setup codec values
        AudioDecoder.setJitterBufferSize(properties.getJitterBufferSize());
        AudioDecoder.setOutputBufferSize(properties.getOutputBufferSize());
        AudioEncoder.setInputBufferSize(properties.getInputBufferSize());

        // Initialize the timing system
        Time.init();

        // Setup a socket address to the server
        try {
            serverSocketAddr = new SocketAddress(properties.getServerAddress(), properties.getServerUdpPort());
        } catch (UnknownHostException ex) {
            log.error("Invalid server address. Exiting!", ex);
            System.exit(0);
        }

        // Create a bundle
        bundle = new ClientBundle();

        // Create the network manager
        try {
            networkManager = new ClientNetworkManager(properties.getTerminalId(), bundle, serverSocketAddr,
                    new SocketAddress(properties.getLocalBindAddress(), properties.getLocalBindPort()),
                    new SocketAddress(properties.getMulticastAddress(), properties.getMulticastPort()),
                    properties.getMulticastTTL(), properties);
            packetReceiver = networkManager.getReceiver();
        } catch (IOException ex) {
            log.error(
                    "The network manager could not be created, possibly due to an invalid multicast socket setting. Exiting!",
                    ex);
            System.exit(0);
        }

        // Create push to talk interfaces
        if (properties.getEventDeviceName().length() > 0) {
            eventSwitch = new EventSwitchController(this, properties.getEventDeviceName());
        }
        if (properties.getFootSwitchInterface().length() > 0) {
            try {
                footSwitch = new FootSwitchController(this, properties.getFootSwitchInterface(),
                        properties.getFootSwitchPollTimeMillis(), properties.isFootSwitchInverted());
            } catch (UnsatisfiedLinkError err) {
                log.error("UnsatisfiedLinkError: " + err.getMessage());
                log.warn("Missing ftsw library (ftsw.dll or libftsw.so)");
                if (!properties.getFootSwitchInterface().equalsIgnoreCase("ftdi"))
                    log.warn("Missing rxtxSerial library (rxtxSerial.dll or librxtxSerial.so)");
                log.warn("The footswitch has been disabled!");
                footSwitch = null;
            } catch (NoClassDefFoundError err) {
                log.warn("Missing ftsw library (ftsw.dll or libftsw.so)");
                if (!properties.getFootSwitchInterface().equalsIgnoreCase("ftdi"))
                    log.warn("Missing rxtxSerial library (rxtxSerial.dll or librxtxSerial.so)");
                log.warn("The footswitch has been disabled!");
                footSwitch = null;
            }
        }

        // Get the selected audio devices
        Mixer.Info[] mixerinfo = AudioSystem.getMixerInfo();

        if ((properties.getSoundOutputDevice() >= mixerinfo.length)
                || (properties.getSoundInputDevice() >= mixerinfo.length)) {
            log.error("Invalid sound device. Exiting!");
            //System.exit(0);
        }

        outputMixer = AudioSystem.getMixer(mixerinfo[properties.getSoundOutputDevice()]);
        inputMixer = AudioSystem.getMixer(mixerinfo[properties.getSoundInputDevice()]);

        if (log.isDebugEnabled()) {
            log.debug("Using output device " + properties.getSoundOutputDevice() + " ("
                    + mixerinfo[properties.getSoundOutputDevice()] + ")");
            log.debug("Using input device " + properties.getSoundInputDevice() + " ("
                    + mixerinfo[properties.getSoundInputDevice()] + ")");
        }

        // Creating sound clips
        clipRingTone = new SoundClip(outputMixer, "data/resources/sounds/phone_ringtone_mono.wav",
                "data/resources/sounds/phone_ringtone_stereo.wav", 1100, properties.getSignalVolumeAdjustment());
        volumeClipList.add(clipRingTone);
        clipRingBack = new SoundClip(outputMixer, "data/resources/sounds/phone_ringback_mono.wav",
                "data/resources/sounds/phone_ringback_stereo.wav", 2100, properties.getSignalVolumeAdjustment());
        volumeClipList.add(clipRingBack);
        clipRingBusy = new SoundClip(outputMixer, "data/resources/sounds/phone_busy_mono.wav",
                "data/resources/sounds/phone_busy_stereo.wav", 400, properties.getSignalVolumeAdjustment());
        volumeClipList.add(clipRingBusy);
        clipWarning = new SoundClip(outputMixer, "data/resources/sounds/radio_error_mono.wav",
                "data/resources/sounds/radio_error_stereo.wav", 1000, properties.getSignalVolumeAdjustment());
        volumeClipList.add(clipWarning);
        clipNotify = new SoundClip(outputMixer, "data/resources/sounds/ding_mono.wav",
                "data/resources/sounds/ding_stereo.wav", 1000, properties.getSignalVolumeAdjustment());
        volumeClipList.add(clipNotify);

        // Create peripheral link
        peripheralLink = null;
        int peripheralLinkPort = properties.getPeripheralLinkPort();
        if (peripheralLinkPort > 0) {
            try {
                peripheralLink = new PeripheralLink(properties.getTerminalId(), peripheralLinkPort);
                peripheralLink.startModule();
            } catch (IOException ex) {
                log.warn("Unable to create peripheral link, disabling!");
                peripheralLink = null;
            }
        }

        // Add shutdown hook
        if (hookThread != null)
            Runtime.getRuntime().removeShutdownHook(hookThread);
        hookThread = new Thread(new Runnable() {
            public void run() {
                stop(true, true);
                shutdown(true);
                sleep(25);
                dbgListThreads();
                log.info("Bye!");
            }
        }, "Thook");
        Runtime.getRuntime().addShutdownHook(hookThread);

        if (Config.CLIENT_SERVERLESS) {
            state = CLIENT_STATE_CONNECTED;
            sessionStart();
        } else {
            // Setup handler for incoming requests and timeouts
            networkManager.setNetworkHandler(this);

            // Try to connect to server
            log.info("Connecting to server on " + properties.getServerAddress() + "...");
            networkManager.serverConnect();
        }

        if (autoTesterEnabled) {
            autoTester = new AutoTester(this);
        }
    }

    public synchronized void shutdown(boolean finalize) {

        if (state != CLIENT_STATE_UNINITIALIZED) {
            state = CLIENT_STATE_UNINITIALIZED;

            log.info("Stopping terminal");

            if (networkManager == null || Config.CLIENT_SERVERLESS) {
                serverDisconnected();
            } else {
                if (networkManager.serverDisconnect()) {
                    semDisconnected = false;
                    try {
                        long t = System.currentTimeMillis();
                        while (!semDisconnected && (System.currentTimeMillis() < (t + TIMEOUT_SHUTDOWN)))
                            wait(100);
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }

        if (finalize) {
            if (timer != null) {
                log.debug("Finalizing");
                timer.cancel();
                timer = null;
            }
        }
    }

    public synchronized void start() {
        try {
            long t = System.currentTimeMillis();
            while (!semStopFinished && (System.currentTimeMillis() < (t + TIMEOUT_STARTSTOP)))
                wait(100);
        } catch (InterruptedException ex) {
        }

        if (state == CLIENT_STATE_CONNECTED) {

            view = null;
            audioRecorder = null;

            log.info("Starting session");

            radioState = RADIO_STATE_IDLE;
            phoneState = PHONE_STATE_IDLE;
            phoneActive = false;
            radioEncoding = false;
            radioDecoding = false;
            radioDecodingList.clear();

            log.debug("Downloading data model for terminal " + properties.getTerminalId() + " from http://"
                    + properties.getServerAddress() + ":" + properties.getServerHttpPort());
            // Get configuration document from server
            String url = "http://" + properties.getServerAddress() + ":" + properties.getServerHttpPort()
                    + "/xml/info?terminal=" + properties.getTerminalId();
            try {
                SAXBuilder builder = new SAXBuilder();
                if (Config.CLIENT_SERVERLESS)
                    model = builder.build(new java.io.File("data/development/client_inforequest.xml"));
                else
                    model = builder.build(new URL(url));
            } catch (Exception ex) {
                log.error("Failed to get or parse the configuration document from " + url.toString(), ex);
                log.debug("Waiting for start session packet from server");
                ex.printStackTrace();
                return;
            }

            if (!validateModel()) {
                log.error("Invalid data model, validation failed!");
                log.debug("Waiting for start session packet from server");
                return;
            }

            // Document is valid
            state = CLIENT_STATE_STARTED;
            log.debug("Data model passed validation test");

            // Add extra nodes
            model.getRootElement().addContent(new Element("TalkButton"));
            model.getRootElement().addContent(new Element("HookButton"));
            Element e1 = new Element("Settings");
            Element e2 = new Element("Name");
            e2.setText("SETTINGS");
            e1.addContent(e2);
            e1.setAttribute("hfac", "0.4");
            model.getRootElement().addContent(e1);

            // Inactivate all phone peers initially
            Iterator iter1 = model.getRootElement().getChild("RoleSetup").getChildren().iterator();
            while (iter1.hasNext()) {
                Element er = (Element) iter1.next();
                Iterator iter2 = er.getChild("PhonePeers").getChildren().iterator();
                while (iter2.hasNext()) {
                    Element err = (Element) iter2.next();
                    err.setAttribute("active", "false");
                }
            }

            // Clear
            encoder = null;
            distributor = null;
            decoderPhone = null;
            decoderForward = null;
            monitorEncoder = null;
            monitorDistributor = null;

            // Get codec type
            String codec = DomTools.getChildText(model.getRootElement(), "Codec", "null", true);

            // Encoder
            if (codec.startsWith("jspeex")) {
                // Parse out the quality from the string "jspeex:<quality 1-10>"
                int quality = 6;
                int idx = codec.indexOf(':');
                if (idx > 0) {
                    try {
                        quality = Integer.parseInt(codec.substring(idx + 1));
                    } catch (Exception ex) {
                        log.warn("Parse error in string " + codec
                                + ", should be \"jspeex:<quality 1-10>\", defaulting to 6!");
                    }
                    if (quality < 1 || quality > 10) {
                        log.warn("Out of range error in string " + codec
                                + ", should be \"jspeex:<quality 1-10>\", defaulting to 6!");
                        quality = 6;
                    }
                }
                encoder = new JSpeexEncoder(inputMixer, quality);
                monitorEncoder = new JSpeexEncoder(inputMixer, quality);
            } else {
                encoder = new NullEncoder(inputMixer);
                monitorEncoder = new NullEncoder(inputMixer);
            }

            // Main distributor
            distributor = new Distributor(properties.getTerminalId(), bundle, networkManager.getSender());
            encoder.setPacketDistributor(distributor);

            // Monitor distributor
            monitorDistributor = new Distributor(properties.getTerminalId(), bundle, networkManager.getSender());
            monitorEncoder.setPacketDistributor(monitorDistributor);

            // Start encoders
            AudioFormat audioFormats[] = encoder.getSupportedAudioFormats();
            int usedAudioFormat = -1;

            // Find which audio format to use
            for (int i = 0; i < audioFormats.length; i++) {
                try {
                    encoder.startModule(i);
                    log.debug("Using input audio format " + audioFormats[i]);
                    usedAudioFormat = i;
                    break;
                } catch (Exception ex) {
                    log.warn("Could not open the input device for format " + audioFormats[i] + "!");
                }
            }

            if (usedAudioFormat < 0) {
                log.error("Unable to start, the input sound device is unavailable!");
                stop(false, false);
                return;
            }

            MixingDataLine monitorDataLine = null;
            if (usedAudioFormat > 0) {
                log.warn("Unable to open the monitor data line, supports mono only!");
                monitorEncoder = null;
                monitorDistributor = null;
            } else {

                // Create a mixing data line for the monitor
                monitorDataLine = new MixingDataLine();

                try {
                    // Start monitor encoder with the same format as the main encoder
                    monitorEncoder.startModule(monitorDataLine, 0);

                } catch (Exception ex) {
                    log.warn("Unable to open the monitor data line, monitoring has been disabled!", ex);
                    monitorDataLine = null;
                    monitorEncoder = null;
                    monitorDistributor = null;
                }
            }

            // Pass a monitor channel to the encoder
            if (monitorDataLine != null) {
                encoder.setMonitorChannel(monitorDataLine.getMixerChannel());
            }

            // Create decoders and packet dispatchers
            if (codec.startsWith("jspeex")) {
                decoderPhone = new JSpeexDecoder(outputMixer, CHANNEL_PHONE);
                decoderForward = new JSpeexDecoder(outputMixer, CHANNEL_FORWARD);
            } else {
                decoderPhone = new NullDecoder(outputMixer, CHANNEL_PHONE);
                decoderForward = new NullDecoder(outputMixer, CHANNEL_FORWARD);
            }

            // Add decoders as packet receiver handlers
            packetReceiver.addDataPacketDispatcher(CHANNEL_PHONE, decoderPhone);
            packetReceiver.addDataPacketDispatcher(CHANNEL_FORWARD, decoderForward);

            // Pass monitor channel to decoders
            if (monitorDataLine != null) {
                decoderPhone.setMonitorChannel(monitorDataLine.getMixerChannel());
                decoderForward.setMonitorChannel(monitorDataLine.getMixerChannel());
            }

            Iterator iter = model.getRootElement().getChild("ChannelSetup").getChildren().iterator();
            while (iter.hasNext()) {
                Element ec = (Element) iter.next();
                int channelId = DomTools.getAttributeInt(ec, "id", 0, true);
                if (channelId > 0) {
                    AudioDecoder decoder;
                    if (codec.startsWith("jspeex")) {
                        decoder = new JSpeexDecoder(outputMixer, channelId);
                    } else {
                        decoder = new NullDecoder(outputMixer, channelId);
                    }

                    // Add decoder as packet receiver handler
                    packetReceiver.addDataPacketDispatcher(channelId, decoder);

                    // Pass monitor channel to decoder
                    if (monitorDataLine != null) {
                        decoder.setMonitorChannel(monitorDataLine.getMixerChannel());
                    }
                }
            }

            // Go through the decoders and create output lines for them
            audioFormats = null;
            usedAudioFormat = -1;
            iter = packetReceiver.getDataPacketDispatcherCollection().iterator();
            while (iter.hasNext()) {
                AudioDecoder decoder = (AudioDecoder) iter.next();

                // First iteration, find which audio format to use
                if (usedAudioFormat < 0) {
                    audioFormats = decoder.getSupportedAudioFormats();
                    for (int i = 0; i < audioFormats.length; i++) {
                        try {
                            decoder.startModule(i);
                            log.debug("Using output audio format " + audioFormats[i]);
                            usedAudioFormat = i;
                            break;
                        } catch (Exception ex) {
                            log.warn("Could not open the output device for format " + audioFormats[i] + "!");
                        }
                    }

                    if (usedAudioFormat < 0) {
                        log.error("Unable to start, the output sound device is unavailable!");
                        stop(false, false);
                        return;
                    }
                }

                // The format to use is known, just start the module
                else {
                    try {
                        decoder.startModule(usedAudioFormat);
                    } catch (LineUnavailableException ex) {
                        log.error("Could not open the output device for format " + audioFormats[usedAudioFormat]
                                + "!");
                        stop(false, false);
                        return;
                    }
                }

                // Add listeners for all radio
                // Also add to volume list
                if (decoder.getDecoderId() > CHANNEL_RADIO_START) {
                    decoder.addListener(this);
                    volumeRadioList.add(decoder);
                }
            }

            // Create a recorder
            audioRecorder = new AudioRecorder(audioFormats[usedAudioFormat], true);

            // Join channels
            Collection<Integer> c = new LinkedList<Integer>();
            // Common
            bundle.addChannel(new ClientChannel(CHANNEL_COMMON));
            c.add(CHANNEL_COMMON);
            iter = model.getRootElement().getChild("ChannelSetup").getChildren().iterator();
            while (iter.hasNext()) {
                Element ec = (Element) iter.next();
                int channelId = DomTools.getAttributeInt(ec, "id", 0, true);
                if (channelId > 0) {
                    ClientChannel channel = new ClientChannel(channelId);
                    bundle.addChannel(channel);
                    c.add(channelId);
                    // Also store the channel element..
                    channel.setElement(ec);
                    // ..and the decoder for fast access
                    AudioDecoder decoder = (AudioDecoder) packetReceiver.getDataPacketDispatcher(channelId);
                    channel.setDecoder(decoder);
                    // finally, add the recorder if the channel has recordable="true"
                    if (DomTools.getAttributeBoolean(ec, "recordable", false, false)) {
                        decoder.setAudioRecorder(audioRecorder);
                        decoder.addListener(audioRecorder); // to get stop events for injecting silence
                    }
                }
            }

            networkManager.sessionConnect(c);

            // Immediately open all channels with state rxtx or rx
            iter = bundle.getChannelCollection().iterator();
            while (iter.hasNext()) {
                ClientChannel channel = (ClientChannel) iter.next();
                if (!DomTools.getAttributeString(channel.getElement(), "state", "off", false).equals("off")) {
                    packetReceiver.openChannel(channel.getId());
                }
            }

            // Force all settings
            Settings settings = Settings.getInstance();
            settingsValueChanged(Settings.ID_MASTER_VOLUME, settings.getMasterVolume());
            settingsValueChanged(Settings.ID_SIGNAL_VOLUME, settings.getSignalVolume());
            settingsValueChanged(Settings.ID_CHPRIO_VOLUME, settings.getChprioVolume());
            settingsValueChanged(Settings.ID_RAPASS_VOLUME, settings.getRapassVolume());
            settingsValueChanged(Settings.ID_CHPRIO_CHOICE, settings.getChprioChoice());
            settingsValueChanged(Settings.ID_WATONE_CHOICE, settings.getWatoneChoice());

            // Create view
            String style = properties.getUserInterfaceStyle().toLowerCase();
            if (!(style.equals("full") || style.equals("slim-phone") || style.equals("slim-horiz")
                    || style.equals("slim-vert") || style.equals("none"))) {
                log.warn("Invalid user interface style (" + style + ") defaulting to full!");
                style = "full";
                properties.setUserInterfaceStyle(style);
            }

            if (!Config.CLIENT_SIZE_FULLSCREEN || !style.equals("full"))
                JFrame.setDefaultLookAndFeelDecorated(true);

            log.debug("Creating view with style: " + style);

            if (properties.getUserInterfaceStyle().equalsIgnoreCase("full")) {
                java.awt.EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        view = new FullView(Controller.getInstance(), properties);
                        view.setModel(model);
                        view.setVisible(true);
                        view.toFront();
                        if (peripheralLink != null)
                            peripheralLink.setView(view);
                    }
                });
            } else if (properties.getUserInterfaceStyle().equalsIgnoreCase("none")) {
                java.awt.EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        view = new FullView(Controller.getInstance(), properties);
                        view.setModel(model);
                        view.setVisible(false);
                        view.toFront();
                        if (peripheralLink != null)
                            peripheralLink.setView(view);
                    }
                });
            } else if (properties.getUserInterfaceStyle().equalsIgnoreCase("slim-phone")) {
                java.awt.EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        view = new PhoneOnlyView(Controller.getInstance(), properties);
                        view.setModel(model);
                        view.setVisible(true);
                        view.toFront();
                        if (peripheralLink != null)
                            peripheralLink.setView(view);
                    }
                });
            } else {
                java.awt.EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        view = new SlimView(Controller.getInstance(), properties);
                        view.setModel(model);
                        view.setVisible(true);
                        view.toFront();
                        if (peripheralLink != null)
                            peripheralLink.setView(view);
                    }
                });
            }

            // Start the push to talk interfaces
            if (properties.getEventDeviceName().length() > 0) {
                eventSwitch.start();
            }
            if (properties.getFootSwitchInterface().length() > 0) {
                if (footSwitch != null) {
                    try {
                        footSwitch.start();
                    } catch (UnsatisfiedLinkError err) {
                        log.error("UnsatisfiedLinkError: " + err.getMessage());
                        log.warn("Missing ftsw library (ftsw.dll or libftsw.so)");
                        if (!properties.getFootSwitchInterface().equalsIgnoreCase("ftdi"))
                            log.warn("Missing rxtxSerial library (rxtxSerial.dll or librxtxSerial.so)");
                        log.warn("The footswitch has been disabled!");
                        footSwitch = null;
                    } catch (NoClassDefFoundError err) {
                        log.warn("Missing ftsw library (ftsw.dll or libftsw.so)");
                        if (!properties.getFootSwitchInterface().equalsIgnoreCase("ftdi"))
                            log.warn("Missing rxtxSerial library (rxtxSerial.dll or librxtxSerial.so)");
                        log.warn("The footswitch has been disabled!");
                        footSwitch = null;
                    } catch (NoSuchPortException ex) {
                        log.warn("The serial port " + properties.getFootSwitchInterface()
                                + " does not exist, the footswitch has been disabled!");
                        footSwitch = null;
                    } catch (PortInUseException ex) {
                        log.warn("The serial port " + properties.getFootSwitchInterface()
                                + " is already in use, the footswitch has been disabled!");
                        footSwitch = null;
                    }
                }
            }

            sleep(25);

            dbgListThreads();

            // The system is not allowed to be stopped immediately after start
            if (timer != null) {
                semStartFinished = false;
                timer.schedule(new TimerTask() {
                    public void run() {
                        Controller c = Controller.getInstance();
                        synchronized (c) {
                            semStartFinished = true;
                            c.notifyAll();
                        }
                    }
                }, DELAY_STARTSTOP);
            }

            if (autoTesterEnabled && autoTester != null) {
                autoTester.startTester(model);
            }
        }
    }

    public synchronized void stop(boolean leaveChannels, boolean waitChannelsLeft) {
        try {
            long t = System.currentTimeMillis();
            while (!semStartFinished && (System.currentTimeMillis() < (t + TIMEOUT_STARTSTOP)))
                wait(100);
        } catch (InterruptedException ex) {
        }

        if (autoTesterEnabled && autoTester != null) {
            autoTester.stopTester();
        }

        semChannelsLeft = true;
        if (state == CLIENT_STATE_STARTED) {

            log.info("Stopping session");

            state = CLIENT_STATE_CONNECTED;

            // Stop all sounds
            playSound(SOUND_NONE);

            if (footSwitch != null)
                footSwitch.stop();

            if (peripheralLink != null)
                peripheralLink.PostRadioSendStop();

            if (view != null) {
                java.awt.EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        view.setVisible(false);
                        view.dispose();
                    }
                });
            }

            if (leaveChannels && bundle != null && !Config.CLIENT_SERVERLESS) {
                // Leave all channels
                Collection<Integer> c = new LinkedList<Integer>();
                Iterator<ClientChannel> iterc = bundle.getChannelCollection().iterator();
                while (iterc.hasNext()) {
                    ClientChannel channel = iterc.next();
                    c.add(new Integer(channel.getId()));
                }

                semChannelsLeft = c.isEmpty();
                if (!semChannelsLeft)
                    networkManager.sessionDisconnect();
            }

            // Stop decoder and close channels
            Iterator<ItemDispatcher> iterd = packetReceiver.getDataPacketDispatcherCollection().iterator();
            while (iterd.hasNext()) {
                AudioDecoder decoder = (AudioDecoder) iterd.next();
                int channelId = decoder.getDecoderId();
                packetReceiver.closeChannel(channelId);
                if (audioRecorder != null) {
                    audioRecorder.stopRecording(channelId);
                }
                decoder.stopModule();
            }

            // Stop encoders
            if (encoder != null) {
                encoder.stopModule();
            }
            if (monitorEncoder != null) {
                monitorEncoder.stopModule();
            }

            packetReceiver.removeDataPacketDispatchers();
            volumeRadioList.clear();
            decoderPhone = null;
            decoderForward = null;
            encoder = null;
            monitorEncoder = null;

            model = null;

            sleep(25);

            if (distributor != null) {
                distributor.removeAll();
                distributor = null;
            }
            if (monitorDistributor != null) {
                monitorDistributor.removeAll();
                monitorDistributor = null;
            }

            log.debug("Session stopped, Waiting for start session packet from server");

            // The system is not allowed to be started immediately after stop
            if (timer != null) {
                semStopFinished = false;
                timer.schedule(new TimerTask() {
                    public void run() {
                        Controller c = Controller.getInstance();
                        synchronized (c) {
                            semStopFinished = true;
                            c.notifyAll();
                        }
                    }
                }, DELAY_STARTSTOP);
            }

            if (leaveChannels && waitChannelsLeft && !semChannelsLeft && !Config.CLIENT_SERVERLESS) {
                try {

                    long t = System.currentTimeMillis();
                    while (!semChannelsLeft && (System.currentTimeMillis() < (t + TIMEOUT_LEAVE)))
                        wait(100);

                    if (!semChannelsLeft)
                        log.warn("Wait for channels left timed out!");

                } catch (InterruptedException ex) {
                    log.warn("Wait for channels left interrupted!");
                }
            }
        }
    }

    public synchronized void rxtxStateUpdated(int channelId, boolean rx, boolean tx) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;
        ClientChannel channel = bundle.getChannel(channelId);
        if (channel != null) {
            Element ec = channel.getElement();

            // Decide on new state
            String newstate = "off";
            if (tx)
                newstate = "rxtx";
            else if (rx)
                newstate = "rx";

            // If channel is a monitor, do not allow rxtx state
            if (newstate.equals("rxtx") && DomTools.getAttributeBoolean(ec, "monitor", false, false)) {
                newstate = "off";
            }

            // Update state in model
            ec.setAttribute("state", newstate);

            // Close sending if state is rx or off
            if (!newstate.equals("rxtx")) {
                ec.setAttribute("send", "false");
                ec.setAttribute("fail", "false");
                encoder.resetChannel(channelId);
            }

            // Close receiving if state is off
            if (newstate.equals("off")) {
                ec.setAttribute("recvp", "false");
                ec.setAttribute("recvs", "false");
                packetReceiver.closeChannel(channelId);
                channel.getDecoder().forceStop();
                recordStateUpdated(channelId, false);
            }

            // Open channel if new state is rx or rxtx
            else {
                packetReceiver.openChannel(channelId);
            }

            // Update view
            if (view != null)
                view.updateRadioView();
        }
    }

    private synchronized void updateRecorder(int channelId) {
        if (bundle == null || state != CLIENT_STATE_STARTED || audioRecorder == null)
            return;

        ClientChannel channel = bundle.getChannel(channelId);
        if (channel != null) {
            Element ec = channel.getElement();

            boolean monitor = DomTools.getAttributeBoolean(ec, "monitor", false, false);
            boolean recording = DomTools.getAttributeBoolean(ec, "recording", false, false);
            boolean monitoring = (monitoredTerminalId > 0);

            if (monitor) {
                ec.setAttribute("monitoring", String.valueOf(monitoring));

                int curMonitoredTerminalId = audioRecorder.getMonitoredTerminalId(channelId);

                if (monitoring && recording) {

                    // If monitored terminal has changed, stop recorder first
                    if (curMonitoredTerminalId > 0 && curMonitoredTerminalId != monitoredTerminalId) {
                        audioRecorder.stopRecording(channelId);
                    }

                    audioRecorder.startRecording(channelId, null, monitoredTerminalId);
                } else {
                    audioRecorder.stopRecording(channelId);
                }
            }

            else {
                if (recording) {
                    String channelName = DomTools.getChildText(ec, "Name", null, false);
                    audioRecorder.startRecording(channelId, channelName, 0);
                } else {
                    audioRecorder.stopRecording(channelId);
                }
            }
        }
    }

    public synchronized void recordStateUpdated(int channelId, boolean recording) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED || audioRecorder == null)
            return;

        ClientChannel channel = bundle.getChannel(channelId);
        if (channel != null) {
            Element ec = channel.getElement();

            // Stop recording if state is off
            if (DomTools.getAttributeString(ec, "state", "off", true).equals("off")) {
                ec.setAttribute("recording", "false");
                audioRecorder.stopRecording(channelId);
            } else {
                ec.setAttribute("recording", String.valueOf(recording));
                updateRecorder(channelId);
            }

            if (view != null)
                view.updateRadioView();
        }
    }

    public synchronized void channelButtonPressed(int channelId) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;
        if (radioState != RADIO_STATE_IDLE)
            return;

        if (!waitChannelAcquiry())
            return;

        // Acquire clicked channel
        radioAcquiredChannelsList.clear();
        if (!Config.CLIENT_SERVERLESS) {
            ClientChannel channel = bundle.getChannel(channelId);
            if (channel != null) {
                Element ec = channel.getElement();

                // Check that the state of the channel is tx
                if (DomTools.getAttributeString(ec, "state", "off", false).equals("rxtx")) {
                    // Try to acquire it
                    radioAcquireCount = 1;
                    networkManager.radioAcquire(channelId);
                    radioState = RADIO_STATE_CHANNEL;
                }
            }
        }
    }

    public synchronized void channelButtonReleased(int channelId) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;
        if (radioState != RADIO_STATE_CHANNEL)
            return;
        releaseAcquiredChannels();
        radioState = RADIO_STATE_IDLE;
    }

    public synchronized void talkButtonPressed(int device) {
        //      if (isaDialog != null) {
        //              isaValueChosen(0);
        //      }
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;

        // Return if a channel button is pressed
        if (radioState == RADIO_STATE_CHANNEL)
            return;

        // BN: HACK controller wants to be able to speak, even if incoming transmission
        if (properties.getOverPowerOtherStations() == 1) {
            log.debug("BN HACK: Controller wants to be able to speak");
            // Release the channel so we can send
            Iterator<ClientChannel> iter = bundle.getChannelCollection().iterator();
            while (iter.hasNext()) {
                ClientChannel channel = iter.next();
                networkManager.radioRelease(channel.getId());
            }
            radioState = RADIO_STATE_IDLE;

            // Set the master volume to 0 so we don't hear others
            Settings s = Settings.getInstance();
            s.setMasterVolume(0);
            adjustVolumes();
        }

        // Only acquire channels if state is idle
        boolean acquireChannels = (radioState == RADIO_STATE_IDLE);

        // Go to next state
        if (device == DEVICE_MOUSE)
            radioState = RADIO_STATE_TALK_MOUSE;
        else if (device == DEVICE_FTSW)
            radioState = RADIO_STATE_TALK_FTSW;

        // Leave if double press, same of difference device
        if (!acquireChannels)
            return;

        if (!waitChannelAcquiry())
            return;

        // Try to acquire all tx channels
        radioAcquiredChannelsList.clear();
        if (!Config.CLIENT_SERVERLESS) {
            radioAcquireCount = 0;
            Iterator<ClientChannel> iter = bundle.getChannelCollection().iterator();
            while (iter.hasNext()) {
                ClientChannel channel = iter.next();
                int channelId = channel.getId();

                if (channelId > CHANNEL_RADIO_START) {
                    Element ec = channel.getElement();

                    // Check that the state of the channel is tx
                    if (DomTools.getAttributeString(ec, "state", "off", false).equals("rxtx")) {
                        radioAcquireCount++;
                        networkManager.radioAcquire(channelId);
                    }
                }
            }
        } else if (peripheralLink != null) {
            // Debug: ServerLess mode
            int[] channelIds = { 1, 2, 3 };
            peripheralLink.PostRadioSendStart(channelIds);
        }
    }

    public synchronized void talkButtonReleased(int device) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;

        // BN: HACK controller wants to be able to speak, even if incoming transmission
        if (properties.getOverPowerOtherStations() == 1) {
            log.debug("BN HACK: Enable volume again");
            // Set the master volume to 100
            Settings s = Settings.getInstance();
            s.setMasterVolume(100);
            adjustVolumes();
        }

        boolean releaseChannels = (radioState == RADIO_STATE_TALK_MOUSE && device == DEVICE_MOUSE)
                || (radioState == RADIO_STATE_TALK_FTSW && device == DEVICE_FTSW);

        if (releaseChannels) {
            releaseAcquiredChannels();
            radioState = RADIO_STATE_IDLE;
        }
    }

    public synchronized void dialButtonClicked(int roleId, int peerId) {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;
        if (phoneState == PHONE_STATE_RINGING) {
            Element eh = model.getRootElement().getChild("HookButton");
            int thisId = DomTools.getAttributeInt(eh, "this", 0, true);
            int otherId = DomTools.getAttributeInt(eh, "other", 0, true);
            if (otherId != peerId) {
                eh.setAttribute("state", "idle");
                networkManager.phoneHangup(thisId, otherId);
                //phoneState = PHONE_STATE_HANGUP_REQ;
                phoneState = PHONE_STATE_IDLE;
            }
        }
        if (phoneState == PHONE_STATE_IDLE) {

            if (roleId == 0 || peerId == 0)
                return;

            // Setup information on hook button
            Element eh = model.getRootElement().getChild("HookButton");
            eh.setAttribute("src", getRoleName(roleId));
            eh.setAttribute("dest", getRoleName(peerId));
            eh.setAttribute("this", String.valueOf(roleId));
            eh.setAttribute("other", String.valueOf(peerId));
            eh.setAttribute("state", "dialing");
            if (view != null)
                view.updatePhoneView();
            networkManager.phoneRing(roleId, peerId);
            phoneState = PHONE_STATE_DIALING_REQ;
        }
    }

    public synchronized void hookButtonClicked() {
        if (bundle == null)
            return;
        if (state != CLIENT_STATE_STARTED)
            return;
        Element eh = model.getRootElement().getChild("HookButton");
        if (phoneState == PHONE_STATE_RINGING) {
            eh.setAttribute("state", "in_call");
            int thisId = DomTools.getAttributeInt(eh, "this", 0, true);
            int otherId = DomTools.getAttributeInt(eh, "other", 0, true);
            networkManager.phoneAnswer(thisId, otherId);
            phoneState = PHONE_STATE_ANSWER_REQ;
        } else if (phoneState == PHONE_STATE_DIALING || phoneState == PHONE_STATE_IN_CALL) {
            eh.setAttribute("state", "idle");
            int thisId = DomTools.getAttributeInt(eh, "this", 0, true);
            int otherId = DomTools.getAttributeInt(eh, "other", 0, true);
            networkManager.phoneHangup(thisId, otherId);
            phoneState = PHONE_STATE_HANGUP_REQ;
        } else if (phoneState == PHONE_STATE_BUSY) {
            eh.setAttribute("state", "idle");
            playSound(SOUND_NONE);
            phoneState = PHONE_STATE_IDLE;
        }
        if (view != null)
            view.updatePhoneView();
    }

    public synchronized void settingsDialogOpen() {
        if (state != CLIENT_STATE_STARTED)
            return;
        // This button is also used to stop the auto tester
        if (autoTesterEnabled) {
            autoTester.stopTester();
            JOptionPane.showMessageDialog(view,
                    "The auto tester has been stopped! The settings dialog\n"
                            + "is unaccessible when the auto tester is enabled!",
                    "Auto tester", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        if (settingsDialog == null) {
            boolean full = properties.getUserInterfaceStyle().equalsIgnoreCase("full");
            if (!full)
                JDialog.setDefaultLookAndFeelDecorated(true);

            settingsDialog = new JDialog(view, "Settings", false);
            settingsDialog.setContentPane(new SettingsPane(this));

            if (full) {
                settingsDialog.setResizable(false);
                settingsDialog.setUndecorated(true);
                Rectangle rect = view.getBounds();
                rect.x += rect.width * 0.1;
                rect.y += rect.height * 0.1;
                rect.width *= 0.8;
                rect.height *= 0.8;
                settingsDialog.setBounds(rect);

                // Hide mouse cursor if stated in the properties file
                if (properties.isMouseCursorHidden())
                    settingsDialog.setCursor(InvisibleCursor.getCursor());
            } else {
                GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
                Rectangle rect = graphicsEnvironment.getMaximumWindowBounds();
                rect.x += rect.width * 0.25;
                rect.y += rect.height * 0.25;
                rect.width *= 0.5;
                rect.height *= 0.5;
                settingsDialog.setBounds(rect);
                settingsDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
                settingsDialog.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        settingsDialogClose();
                    }
                });
            }

            settingsDialog.setVisible(true);
        }
    }

    public synchronized void settingsDialogClose() {
        if (settingsDialog != null) {
            settingsDialog.setVisible(false);
            settingsDialog = null;
        }
    }

    public synchronized void settingsValueChanged(int id, int value) {
        if (state != CLIENT_STATE_STARTED)
            return;
        Settings s = Settings.getInstance();
        switch (id) {
        case (Settings.ID_MASTER_VOLUME): {
            s.setMasterVolume(value);
            adjustVolumes();
            break;
        }
        case (Settings.ID_SIGNAL_VOLUME): {
            float signalVolume = (float) value / (float) Settings.DEF_SIGNAL_VOLUME;
            Iterator<VolumeAdjustable> iter = volumeClipList.iterator();
            while (iter.hasNext())
                iter.next().setVolume(signalVolume);
            s.setSignalVolume(value);
            break;
        }
        case (Settings.ID_CHPRIO_VOLUME): {
            s.setChprioVolume(value);
            adjustVolumes();
            break;
        }
        case (Settings.ID_RAPASS_VOLUME): {
            s.setRapassVolume(value);
            adjustVolumes();
            break;
        }
        case (Settings.ID_CHPRIO_CHOICE): {
            s.setChprioChoice(value);
            adjustVolumes();
            break;
        }
        case (Settings.ID_WATONE_CHOICE): {
            clipWarningOn = (value == Settings.WATONE_ON);
            s.setWatoneChoice(value);
            break;
        }
        default:
            log.error("Settings id is invalid!");
        }
    }

    /**
     * Open the ISA dialog (if the radio has been silent for at least 2 seconds)
     * @return true if the dialog was opened
     */
    public boolean isaDialogOpen(int isaNumChoices, boolean isaExtendedMode, String isakeytext[]) {
        final int radioSilentLimit = 500; // Do not show ISA dialog until at least half a second of silence
        if (view == null)
            return true; // This might happen if ISA request when terminal is starting up, don't open ISA dialog
        if (isaDialog == null) {
            // Is radio or phone active?
            boolean codecActive = (radioEncoding || radioDecoding || phoneActive);
            // Wait for 2 seconds of radio silence
            if (codecActive || (System.currentTimeMillis() - radioIdleTime) < radioSilentLimit) {
                // Ignore ISA request if too busy
                return false;
            }

            boolean full = properties.getUserInterfaceStyle().equalsIgnoreCase("full");
            if (!full)
                JDialog.setDefaultLookAndFeelDecorated(true);

            isaDialog = new JDialog(view, "Workload", false);
            isaDialog.setContentPane(
                    new ISAPane(Controller.getInstance(), isaNumChoices, isaExtendedMode, isakeytext));

            if (full) {
                isaDialog.setResizable(false);
                isaDialog.setUndecorated(true);
                Rectangle rect = view.getBounds();
                if (isaExtendedMode) {
                    rect.x += rect.width * 0.3;
                    rect.y += rect.height * 0.3;
                    rect.width *= 0.4;
                    rect.height *= 0.5;
                } else {
                    rect.x += rect.width * 0.2;
                    rect.y += rect.height * 0.4;
                    rect.width *= 0.6;
                    rect.height *= 0.15;
                }
                isaDialog.setBounds(rect);

                // Hide mouse cursor if stated in the properties file
                if (properties.isMouseCursorHidden())
                    isaDialog.setCursor(InvisibleCursor.getCursor());
            } else {
                GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
                Rectangle rect = graphicsEnvironment.getMaximumWindowBounds();
                if (isaExtendedMode) {
                    rect.x = (int) (rect.width - (rect.width * 0.2));
                    rect.y = (int) (rect.height - (rect.height * 0.25));
                    rect.width *= 0.2;
                    rect.height *= 0.25;
                } else {
                    rect.x = (int) (rect.width - (rect.width * 0.3));
                    rect.y = (int) (rect.height - (rect.height * 0.1));
                    rect.width *= 0.3;
                    rect.height *= 0.1;
                }
                isaDialog.setBounds(rect);
                isaDialog.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        isaValueChosen(0);
                    }
                });
            }
            isaDialog.setVisible(true);
            isaReqStartTime = System.currentTimeMillis();
            clipNotify.playOnce();
        }
        return true;
    }

    /**
     * Close the ISA dialog
     */
    public void isaDialogClose() {
        if (isaDialog != null) {
            isaDialog.setVisible(false);
            isaDialog = null;
        }
    }

    /**
     * User has chosen an ISA value, send response to server
     */
    public void isaValueChosen(int value) {
        if (isaDialog != null && isaDialog.isShowing()) {
            log.debug("ISA value chosen: " + value);
            isaDialogClose();
            networkManager.sendIsaResponse(value, (System.currentTimeMillis() - isaReqStartTime));
        } else {
            log.debug("ISA dialog not open - ignore");
        }

    }

    public synchronized void closeButtonClicked() {
        if (state != CLIENT_STATE_STARTED)
            return;
        if (Config.CLIENT_EXIT_DIALOG) {
            int res = JOptionPane.showConfirmDialog(view, "Are you sure you want to exit the terminal application?",
                    "Exit?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
            if (res == JOptionPane.YES_OPTION) {
                stop(true, true);
                shutdown(true);
            }
        } else {
            stop(true, true);
            shutdown(true);
        }
    }

    public synchronized void serverConnected(boolean accepted) {
        if (accepted) {
            log.info("Connected to server " + serverSocketAddr);
            state = CLIENT_STATE_CONNECTED;
            dbgListThreads();
            // Wait for control start to create visuals
            log.debug("Waiting for start session packet from server");
        } else {
            if (reconnectPrintCount > 0) {
                log.info("Unable to connect to server");
            }
            if (reconnectPrintCount >= 0) {
                reconnectPrintCount--;
            }
            if (timer != null) {
                if (reconnectPrintCount > 0)
                    log.info("Reconnecting in " + (DELAY_RECONNECT / 1000) + " seconds...");
                timer.schedule(new TimerTask() {
                    public void run() {
                        if (networkManager != null) {
                            if (reconnectPrintCount > 0)
                                log.info("Connecting to server...");
                            networkManager.serverConnect();
                        }
                    }
                }, DELAY_RECONNECT);
                if (reconnectPrintCount == 0) {
                    log.info("Entering silent mode, still trying to connect to server...");
                    reconnectPrintCount = -1;
                }
            } else {
                log.error("Unexpected shutdown, timer is null!");
                shutdown(true);
            }
        }
    }

    public synchronized void serverDisconnected() {
        networkManager.setNetworkHandler(null);

        // Close peripheral link
        if (peripheralLink != null)
            peripheralLink.stopModule();

        // Close sound clips
        clipRingTone.close();
        clipRingBack.close();
        clipRingBusy.close();
        clipWarning.close();
        clipNotify.close();
        SoundClip.closeCommonTimer();
        outputMixer.close();
        inputMixer.close();
        volumeClipList.clear();

        state = CLIENT_STATE_UNINITIALIZED;
        clear();

        // Notify shutdown hook
        notifyAll();
    }

    public synchronized void sessionStart() {
        if (state != CLIENT_STATE_STARTED && timer != null) {
            log.debug("Start session packet received");
            timer.schedule(new TimerTask() {
                public void run() {
                    start();
                }
            }, DELAY_START + (int) (2 * Math.random() * DELAY_START_VARIANCE) - DELAY_START_VARIANCE);
        }
    }

    public synchronized void sessionStop() {
        if (state == CLIENT_STATE_STARTED) {
            log.debug("Stop session packet received");
            stop(true, false);
        }
    }

    public synchronized void sessionClose() {
        log.debug("Close session packet received");
        // Ok to call stop and shutdown directly after each other here
        // since the server is stopping anyway.. No synchronization
        // problems then..
        stop(false, false);
        shutdown(timer == null);
        if (timer != null) {
            log.info("Reconnecting in " + (DELAY_RECONNECT / 1000) + " seconds...");
            timer.schedule(new TimerTask() {
                public void run() {
                    int id = properties.getTerminalId();
                    init(id);
                }
            }, DELAY_RECONNECT);
        }
    }

    public synchronized void sessionConnected() {
    }

    public synchronized void sessionDisconnected() {
        bundle.clear();
        semChannelsLeft = true;
        notifyAll();
    }

    public synchronized void infoPacket(Packet packet) {

        // Update role activity
        int[] roleActivityArray = packet.getAttributeList(Packet.ATTR_ROLE_ACTIVITY);
        if (state == CLIENT_STATE_STARTED && model != null && roleActivityArray != null) {
            log.debug("Received info packet ROLE_ACTIVITY");
            // Update all peers (online and in call info)
            Iterator iter1 = model.getRootElement().getChild("RoleSetup").getChildren().iterator();
            while (iter1.hasNext()) {
                Element er = (Element) iter1.next();
                Iterator iter2 = er.getChild("PhonePeers").getChildren().iterator();
                while (iter2.hasNext()) {
                    Element err = (Element) iter2.next();
                    int id = DomTools.getAttributeInt(err, "id", 0, false);
                    for (int i : roleActivityArray) {
                        if ((i & Packet.ATTR_ROLE_ACTIVITY_MASK_ID) == id) {
                            err.setAttribute("active",
                                    String.valueOf((i & Packet.ATTR_ROLE_ACTIVITY_FLAG_AVAILABLE) != 0));
                            err.setAttribute("line",
                                    ((i & Packet.ATTR_ROLE_ACTIVITY_FLAG_INCALL) != 0) ? "3" : "1");
                            err.setAttribute("blink", "false");
                        }
                    }
                }
            }

            if (view != null)
                view.updatePhoneView();
        }

        // Monitoring terminal
        else if (packet.getAttributeBool(Packet.ATTR_MONITOR_SINK_START)) {
            log.debug("Received info packet MONITOR_SINK_START");
            monitoredTerminalId = packet.getSourceId();
            updateRecorder(packet.getAttributeInt(Packet.ATTR_CHANNEL));
            if (view != null)
                view.updateRadioView();
        } else if (packet.getAttributeBool(Packet.ATTR_MONITOR_SINK_STOP)) {
            log.debug("Received info packet MONITOR_SINK_STOP");
            monitoredTerminalId = 0;
            updateRecorder(packet.getAttributeInt(Packet.ATTR_CHANNEL));
            if (view != null)
                view.updateRadioView();
        }

        // Monitoring terminal
        else if (packet.getAttributeBool(Packet.ATTR_MONITOR_SOURCE_START)) {
            log.debug("Received info packet MONITOR_SOURCE_START");
            monitoringTerminalId = packet.getSourceId();
            monitorChannelId = packet.getAttributeInt(Packet.ATTR_CHANNEL);
            updateMonitor();
        } else if (packet.getAttributeBool(Packet.ATTR_MONITOR_SOURCE_STOP)) {
            log.debug("Received info packet MONITOR_SOURCE_STOP");
            monitoringTerminalId = 0;
            monitorChannelId = 0;
            updateMonitor();
        }
    }

    public synchronized void radioAcquired(ClientChannel channel, boolean accepted) {
        if (model == null)
            return;

        boolean startProcessing = false;

        // Re-acquire
        if (radioReacquireCount > 0) {
            radioReacquireCount--;

            if (accepted) {

                // Add to acquired list and remove from unacquired list
                radioAcquiredChannelsList.add(channel);
                radioUnacquiredChannelsList.remove(channel);

                // Set busy indicator and clear error indicator on channel
                Element ec = channel.getElement();
                ec.setAttribute("send", "true");
                ec.setAttribute("fail", "false");

                // If all channels are acquired
                if (radioUnacquiredChannelsList.isEmpty()) {
                    startProcessing = true;
                    if (radioReacquireTask != null) {
                        radioReacquireTask.cancel();
                    }
                }
            }
        }

        // Normal acquire
        else if (radioAcquireCount > 0) {
            radioAcquireCount--;

            if (accepted) {
                radioAcquiredChannelsList.add(channel);

                // Set busy indicator on channel
                channel.getElement().setAttribute("send", "true");
            } else {
                radioUnacquiredChannelsList.add(channel);

                // Set error indicator
                channel.getElement().setAttribute("fail", "true");
            }

            // All channels have been acquired
            if (radioAcquireCount == 0) {
                if (radioUnacquiredChannelsList.isEmpty())
                    startProcessing = true;

                else {
                    // Talk button is pressed with error, show fail
                    if (radioState == RADIO_STATE_TALK_MOUSE || radioState == RADIO_STATE_TALK_FTSW) {
                        model.getRootElement().getChild("TalkButton").setAttribute("state", "fail");
                    }

                    if (!radioUnacquiredChannelsList.isEmpty()) {

                        // Create a new task to periodically try to acquire channels
                        radioReacquireTask = new TimerTask() {
                            public void run() {
                                Controller c = Controller.getInstance();
                                synchronized (c) {
                                    // Dont reacquire if channels are being released
                                    // or if still trying to reacquire from last tick
                                    if (radioReleaseCount > 0 || radioReacquireCount > 0)
                                        return;

                                    radioReacquireCount = 0;
                                    Iterator<ClientChannel> iter = radioUnacquiredChannelsList.iterator();
                                    while (iter.hasNext()) {
                                        ClientChannel channel = iter.next();
                                        radioReacquireCount++;
                                        networkManager.radioAcquire(channel.getId());
                                    }
                                }
                            }
                        };

                        // Schedule the task
                        if (timer != null)
                            timer.schedule(radioReacquireTask, PERIOD_REACQUIRE, PERIOD_REACQUIRE);
                        else
                            log.error("Unable to schedule reacquire task, timer is not available!");
                    } else
                        log.error("Radio acquirement failed but there are no unacquired channels in list!");

                    // Audio error signal
                    if (clipWarningOn)
                        clipWarning.playOnce();

                    radioEncoding = false;

                    updateMonitor();
                }
            }
        }

        // Error
        else {
            log.error("radioAcquired was called without (re)acquire count");
        }

        if (startProcessing) {

            if (radioEncoding)
                log.error("Trying to start encoder when radioEncoding is already true!");

            if (radioAcquiredChannelsList.isEmpty())
                log.error("Trying to start encoder without any acquired channels!");

            if (!radioUnacquiredChannelsList.isEmpty())
                log.error("Trying to start encoder with remaining unacquired channels!");

            // Talk button is pressed successfully, show busy
            if (radioState == RADIO_STATE_TALK_MOUSE || radioState == RADIO_STATE_TALK_FTSW) {
                model.getRootElement().getChild("TalkButton").setAttribute("state", "send");
            }

            // Prepare channels list for peripheral link messages
            int i = 0;
            int[] channelIds = new int[radioAcquiredChannelsList.size()];

            // Add all acquired channels to distributor
            Iterator<ClientChannel> iter = radioAcquiredChannelsList.iterator();
            while (iter.hasNext()) {
                int channelId = iter.next().getId();
                channelIds[i] = channelId;
                distributor.addRadioChannel(channelId);
                i++;
            }

            // Start sending voice packets, start encoder if phone has not already started it
            if (!phoneActive)
                encoder.startProcessing();

            // Send message to peripheral
            if (peripheralLink != null)
                peripheralLink.PostRadioSendStart(channelIds);

            radioEncoding = true;
            updateMonitor();
        }

        if (view != null)
            view.updateRadioView();

        notifyAll();
    }

    public synchronized void radioReleased(ClientChannel channel) {
        radioReleaseCount--;
        notifyAll();
        radioIdleTime = System.currentTimeMillis();
    }

    public synchronized void phoneRingOutgoing(ClientTerminal destTerminal, int destRoleId, int sourceRoleId,
            boolean accepted) {
        if (model == null)
            return;

        if (phoneState == PHONE_STATE_DIALING_REQ) {
            if (accepted) {
                // Destination phone is ringing
                updateLocalRoleActivity(sourceRoleId, destRoleId, true, true);
                playSound(SOUND_RINGBACK);
                phoneState = PHONE_STATE_DIALING;
            } else {
                // Phone line was busy
                Element eh = model.getRootElement().getChild("HookButton");
                eh.setAttribute("state", "busy");
                if (view != null)
                    view.updatePhoneView();
                playSound(SOUND_RINGBUSY);
                phoneState = PHONE_STATE_BUSY;
            }
        } else {
            log.error("phoneRingOutgoing called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void phoneRingIncoming(ClientTerminal sourceTerminal, int sourceRoleId, int destRoleId) {
        if (model == null)
            return;

        if (state != CLIENT_STATE_STARTED)
            return;
        if (phoneState == PHONE_STATE_IDLE || phoneState == PHONE_STATE_BUSY) {
            Element eh = model.getRootElement().getChild("HookButton");
            eh.setAttribute("src", getRoleName(sourceRoleId));
            eh.setAttribute("dest", getRoleName(destRoleId));
            eh.setAttribute("this", String.valueOf(destRoleId));
            eh.setAttribute("other", String.valueOf(sourceRoleId));
            eh.setAttribute("state", "ringing");
            updateLocalRoleActivity(destRoleId, sourceRoleId, true, true);
            playSound(SOUND_RINGTONE);
            phoneState = PHONE_STATE_RINGING;
        } else {
            log.error("phoneRingIncoming called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void phoneAnswerOutgoing(ClientTerminal destTerminal, int destRoleId, int sourceRoleId,
            boolean accepted) {
        if (model == null)
            return;

        if (phoneState == PHONE_STATE_ANSWER_REQ) {
            Element eh = model.getRootElement().getChild("HookButton");
            eh.setAttribute("state", "in_call");
            updateLocalRoleActivity(sourceRoleId, destRoleId, true, false);
            playSound(SOUND_NONE);
            phoneState = PHONE_STATE_IN_CALL;
            if (accepted) {
                if (destTerminal != null) {
                    packetReceiver.openChannel(CHANNEL_PHONE);
                    packetReceiver.openChannelForward(destTerminal);
                    phoneActive = true;

                    distributor.setConfinedRecipient(destTerminal, CHANNEL_PHONE);
                    if (!radioEncoding)
                        encoder.startProcessing();

                    adjustVolumes();
                    updateMonitor();
                } else
                    log.warn("destTerminal is null in phoneAnswerOutgoing!");
            } else {
                // Hang up phone
                hookButtonClicked();
            }
        } else {
            log.error("phoneAnswerOutgoing called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void phoneAnswerIncoming(ClientTerminal sourceTerminal, int sourceRoleId, int destRoleId) {
        if (model == null)
            return;

        if (state != CLIENT_STATE_STARTED)
            return;
        if (phoneState == PHONE_STATE_DIALING) {
            Element eh = model.getRootElement().getChild("HookButton");
            eh.setAttribute("state", "in_call");
            updateLocalRoleActivity(destRoleId, sourceRoleId, true, false);
            playSound(SOUND_NONE);
            if (sourceTerminal != null) {
                packetReceiver.openChannel(CHANNEL_PHONE);
                packetReceiver.openChannelForward(sourceTerminal);
                phoneActive = true;

                distributor.setConfinedRecipient(sourceTerminal, CHANNEL_PHONE);
                if (!radioEncoding)
                    encoder.startProcessing();

                adjustVolumes();
                updateMonitor();
            } else
                log.error("sourceTerminal is null in phoneAnswerIncoming");

            phoneState = PHONE_STATE_IN_CALL;
        } else if (phoneState == PHONE_STATE_HANGUP_REQ) {
            // Other peer answered the phone when this peer requested to hangup
        } else {
            log.error("phoneAnswerIncoming called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void phoneHangupOutgoing(ClientTerminal destTerminal, int destRoleId, int sourceRoleId) {
        if (model == null)
            return;

        if (phoneState == PHONE_STATE_HANGUP_REQ || phoneState == PHONE_STATE_DIALING_REQ) { // It is possible for receiver to dial another phone instead of answer a ring.
                                                                                             // If so - send hangup to "dissed" dialer and keep own phone state PHONE_STATE_DIALING_REQ
            updateLocalRoleActivity(sourceRoleId, destRoleId, false, false);
            playSound(SOUND_NONE);
            stopPhone();
            if (phoneState != PHONE_STATE_DIALING_REQ) {
                phoneState = PHONE_STATE_IDLE;
            }
        } else {
            log.error("phoneHangupOutgoing called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void phoneHangupIncoming(ClientTerminal sourceTerminal, int sourceRoleId, int destRoleId) {
        if (model == null)
            return;

        if (state != CLIENT_STATE_STARTED)
            return;
        if (phoneState == PHONE_STATE_IN_CALL || phoneState == PHONE_STATE_RINGING
                || phoneState == PHONE_STATE_DIALING) {
            Element eh = model.getRootElement().getChild("HookButton");
            eh.setAttribute("state", "idle");
            updateLocalRoleActivity(destRoleId, sourceRoleId, false, false);
            playSound(SOUND_NONE);
            stopPhone();
            phoneState = PHONE_STATE_IDLE;
        } else if (phoneState == PHONE_STATE_IDLE) {
            // Both peers hung up at the same time, already back in idle state
        } else {
            log.error("phoneHangupIncoming called in wrong state [" + phoneState + "]");
        }
    }

    public synchronized void timeout() {
        log.error("Unexpected timeout has occured, disconnected from server!");
        stop(false, false);
        shutdown(timer == null);
        if (timer != null) {
            log.info("Reconnecting in " + (DELAY_RECONNECT / 1000) + " seconds...");
            timer.schedule(new TimerTask() {
                public void run() {
                    int id = properties.getTerminalId();
                    init(id);
                }
            }, DELAY_RECONNECT);
        }
    }

    public synchronized void startDecoding(AudioDecoder source, int[] channelIdArray) {

        int decoderId = source.getDecoderId();

        // prefer channel, this channel will always be used before other open channels
        packetReceiver.preferChannel(decoderId);

        // update visuals
        for (int i : channelIdArray) {
            ClientChannel channel = bundle.getChannel(i);
            if (channel != null) {
                if (channel.getId() == decoderId) {
                    channel.getElement().setAttribute("recvp", "true");
                } else {
                    channel.getElement().setAttribute("recvs", "true");
                }
            }
        }

        radioDecodingList.add(source);
        radioDecoding = !radioDecodingList.isEmpty();
        adjustVolumes();
        updateMonitor();

        if (view != null)
            view.updateRadioView();

    }

    public synchronized void stopDecoding(AudioDecoder source, int[] channelIdArray) {

        int decoderId = source.getDecoderId();

        // unprefer channel
        packetReceiver.unpreferChannel(decoderId);

        // update visuals
        for (int i : channelIdArray) {
            ClientChannel channel = bundle.getChannel(i);
            if (channel != null) {
                if (channel.getId() == decoderId) {
                    channel.getElement().setAttribute("recvp", "false");
                } else {
                    channel.getElement().setAttribute("recvs", "false");
                }
            }
        }

        radioDecodingList.remove(source);
        radioDecoding = !radioDecodingList.isEmpty();
        adjustVolumes();
        updateMonitor();

        if (view != null)
            view.updateRadioView();
        radioIdleTime = System.currentTimeMillis();
    }

}