Difian/DifianApp/src/main/java/com/sonova/difian/communication/fittingconnection/FittingConnectionManagerCallback.java
public interface FittingConnectionManagerCallback
{
    void fittingConnectionManagerStateChanged(FittingConnectionManagerState state);
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/chat/ChatManagerCallback.java
public interface ChatManagerCallback
{
    void chatManagerStateChanged();
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/fittingconnection/FittingConnectionState.java
public enum FittingConnectionState
{
    NOT_CONNECTED, CONNECTING_SERIAL, CONNECTING_NETWORK, CONNECTING_WAITING, CONNECTED, STOPPING
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/timer/SessionTimer.java
public final class SessionTimer
{
    private final Object _lockObject = new Object();
    private long _secondOffset;
    private Date _partStartTime;

    public void reset()
    {
        synchronized (_lockObject)
        {
            _secondOffset = 0;
            _partStartTime = null;
        }
    }

    public void start()
    {
        synchronized (_lockObject)
        {
            if (_partStartTime == null)
            {
                _partStartTime = new Date();
            }
        }
    }

    public void pause()
    {
        synchronized (_lockObject)
        {
            if (_partStartTime != null)
            {
                Date now = new Date();
                long seconds = (now.getTime() - _partStartTime.getTime()) / 1000;

                _secondOffset += seconds;
                _partStartTime = null;
            }
        }
    }

    public long getSessionSeconds()
    {
        synchronized (_lockObject)
        {
            long result = _secondOffset;
            if (_partStartTime != null)
            {
                Date now = new Date();
                result += (now.getTime() - _partStartTime.getTime()) / 1000;
            }
            return result;
        }
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/chat/ChatManager.java
public final class ChatManager
{
    private static final String TAG = ChatManager.class.getName();
    private static final int RX_SOCKET_COUNT = 2;
    private static final int TX_SOCKET_COUNT = 1;
    private static final int SOCKET_TIMEOUT = 10000;
    private final ChatManagerCallback _delegate;
    private final Object _lockObject = new Object();
    private final Object _txLockObject = new Object();
    private final SocketFactory _sslSocketFactory = SSLSocketFactory.getDefault();
    private final Pattern _locationPattern = Pattern.compile("^Location\\: https://([a-z0-9\\-\\.]+)\\:443/[a-z0-9]+\\_\\-1$", Pattern.CASE_INSENSITIVE);
    private final Pattern _chatMessagePattern = Pattern.compile("^[\u0020-\u7dff\ue000-\ufffd]*$", 0);
    // Guarded by _lockObject.
    private final AbstractList<YalerSSLServerSocket> _rxChatListenerSockets = new ArrayList<YalerSSLServerSocket>(RX_SOCKET_COUNT);
    // Guarded by _lockObject.
    private final AbstractList<Socket> _rxChatSockets = new ArrayList<Socket>();
    // Guarded by _lockObject.
    private final AbstractList<Socket> _txChatSockets = new ArrayList<Socket>(TX_SOCKET_COUNT);
    // Guarded by _lockObject.
    private final ArrayList<ChatMessage> _chatHistory = new ArrayList<ChatMessage>(4);
    // Guarded by _lockObject, _txLockObject.
    private final Queue<ChatMessage> _pendingSends = new ArrayDeque<ChatMessage>(2);
    // Guarded by _lockObject, _txLockObject.
    private boolean _pendingAudiologistInfo;
    // Guarded by _lockObject.
    private String _id;
    // Guarded by _lockObject.
    private String _relayHost;
    // Guarded by _lockObject.
    private AudiologistInfo _audiologistInfo = AudiologistInfo.EMPTY;
    // Guarded by _lockObject.
    private HiMuteStatus _muteStatusLeft = HiMuteStatus.UNDEFINED;
    // Guarded by _lockObject.
    private HiMuteStatus _muteStatusRight = HiMuteStatus.UNDEFINED;
    // Guarded by _lockObject.
    private boolean _isTyping;

    public ChatManager(ChatManagerCallback delegate)
    {
        synchronized (_lockObject)
        {
            Contract.check(delegate != null);
            _delegate = delegate;
        }
    }

    public void startup(String id, String relayHost)
    {
        synchronized (_txLockObject) {
            synchronized (_lockObject) {
                Log.v(TAG, String.format("startup, id = %s", id));
                Contract.check(id != null);
                Contract.check(relayHost != null);

                if (!id.equals(_id) || !relayHost.equals(_relayHost)) {
                    closeSockets();
                    _id = id;
                    _relayHost = relayHost;
                    openSockets();
                    requestAudiologistInfo();
                }
            }
        }
    }

    private void closeSockets()
    {
        synchronized (_lockObject)
        {
            for (YalerSSLServerSocket s : _rxChatListenerSockets)
            {
                s.close();
            }
            for (Socket s : _rxChatSockets)
            {
                try
                {
                    s.close();
                }
                catch (IOException ignored)
                {
                }
            }
            for (Socket s : _txChatSockets)
            {
                try
                {
                    s.close();
                }
                catch (IOException ignored)
                {
                }
            }
            _rxChatListenerSockets.clear();
            _rxChatSockets.clear();
            _txChatSockets.clear();
        }
    }

    private void openSockets()
    {
        synchronized (_lockObject)
        {
            Contract.check(_id != null);

            while (_rxChatListenerSockets.size() < RX_SOCKET_COUNT)
            {
                YalerSSLServerSocket chatListener = new YalerSSLServerSocket(_relayHost, 443, String.format("%s_1", _id));
                _rxChatListenerSockets.add(chatListener);
                startListening(chatListener, _id);
            }

            while (_txChatSockets.size() < TX_SOCKET_COUNT)
            {
                Socket socket = SocketHelpers.createSocket(_sslSocketFactory, SOCKET_TIMEOUT);
                _txChatSockets.add(socket);
                startTx(socket, _id);
            }
        }
    }

    private void startTx(final Socket initialSocket, final String id)
    {
        Log.i(TAG, "startTx");

        synchronized (_lockObject)
        {
            new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        Log.v(TAG, "TX thread active.");

                        Socket socket = initialSocket;
                        String currentId;
                        String currentHost;
                        synchronized (_lockObject)
                        {
                            currentId = _id;
                            currentHost = _relayHost;
                        }

                        // Pending message.
                        Message m = null;

                        while (id.equals(currentId))
                        {
                            boolean ok = true;
                            boolean resetHost = true;

                            // Connect socket.
                            Log.d(TAG, String.format("Connecting socket for TX (host = %s).", currentHost));

                            try
                            {
                                socket.connect(new InetSocketAddress(currentHost, 443), SOCKET_TIMEOUT);
                            }
                            catch (IOException e)
                            {
                                Log.w(TAG, "Socket.connect failed.", e);
                                ok = false;
                            }

                            InputStream is = null;
                            OutputStream os = null;

                            if (ok)
                            {
                                try
                                {
                                    is = socket.getInputStream();
                                    os = socket.getOutputStream();
                                }
                                catch (IOException e)
                                {
                                    ok = false;
                                }
                            }
                            else
                            {
                                Log.w(TAG, "Clearing pending sends.");
                                synchronized (_txLockObject) // ok to take _txLockObject: _lockObject is not taken
                                {
                                    for (ChatMessage cm : _pendingSends)
                                    {
                                        while (!cm.isFailed())
                                        {
                                            cm.retry();
                                        }
                                    }
                                    if ((m != null) && (m.getType() == MessageType.SMART_REPLY))
                                    {
                                        SmartReplyMessage sm = (SmartReplyMessage)m;
                                        while (!sm.isFailed())
                                        {
                                            sm.retry();
                                        }
                                        m = null;
                                    }
                                    _pendingSends.clear();
                                    _txLockObject.notifyAll();
                                }
                                _delegate.chatManagerStateChanged();
                            }

                            // Message processing loop.
                            while (ok)
                            {
                                // Check if current message delivery should be attempted again.
                                if ((m != null) && (m.getType() == MessageType.SMART_REPLY))
                                {
                                    SmartReplyMessage sm = (SmartReplyMessage)m;
                                    Log.i(TAG, "Retrying delivery of chat message...");
                                    synchronized (_txLockObject) // ok to take _txLockObject: _lockObject is not taken
                                    {
                                        sm.retry();
                                        if (sm.isFailed())
                                        {
                                            m = null;
                                        }
                                    }
                                    _delegate.chatManagerStateChanged();
                                }
                                else if ((m != null) && (m.getType() == MessageType.USER_INFO_REQUEST))
                                {
                                    Log.i(TAG, "Cancelling user info request message in order to give potential smart messages priority...");
                                    m = null;
                                }

                                // Get message.
                                if (m == null)
                                {
                                    m = getMessage(id);
                                    Log.v(TAG, "Processing message " + m);
                                }

                                // Transmit.
                                if (m != null)
                                {
                                    Log.i(TAG, String.format("Sending message: %s", m));
                                    try
                                    {
                                        MessageWriter.getInstance().transmit(os, currentHost, id, m);
                                    }
                                    catch (IOException e)
                                    {
                                        ok = false;
                                    }
                                }
                                else
                                {
                                    ok = false;
                                }

                                // Receive ack.
                                if (ok && (m != null))
                                {
                                    try
                                    {
                                        String headerLine = StreamHelpers.readLine(is);
                                        Log.i(TAG, String.format("Received response code: %s", headerLine));

                                        String http100 = "HTTP/1.1 100 Continue";
                                        String http200 = "HTTP/1.1 200 OK";
                                        String http307 = "HTTP/1.1 307";

                                        if (headerLine.startsWith(http100) || headerLine.startsWith(http200))
                                        {
                                            // Message transmitted successfully.

                                            if (m.getType() == MessageType.SMART_REPLY)
                                            {
                                                synchronized (_txLockObject) // ok to take _txLockObject: _lockObject is not taken
                                                {
                                                    SmartReplyMessage srm = (SmartReplyMessage)m;
                                                    srm.ack();
                                                }
                                                _delegate.chatManagerStateChanged();
                                            }

                                            m = null;
                                            resetHost = false;
                                        }
                                        else
                                        {
                                            resetHost = true;
                                        }

                                        if (!headerLine.startsWith(http200))
                                        {
                                            // Connection closed.
                                            ok = false;
                                        }

                                        if (headerLine.startsWith(http307))
                                        {
                                            // Redirect.
                                            resetHost = false;

                                            do
                                            {
                                                headerLine = StreamHelpers.readLine(is);
                                                if (headerLine.startsWith("Location: "))
                                                {
                                                    Matcher matcher = _locationPattern.matcher(headerLine);
                                                    if (matcher.matches() && (matcher.groupCount() == 1))
                                                    {
                                                        currentHost = matcher.group(1);
                                                        Log.i(TAG, String.format("Redirected to new host: %s ", currentHost));
                                                    }
                                                    else
                                                    {
                                                        resetHost = true;
                                                    }
                                                }
                                            } while (!headerLine.equals(""));
                                            if (currentHost == null)
                                            {
                                                resetHost = true;
                                            }
                                        }
                                        else
                                        {
                                            StreamHelpers.consumeHttpHeaders(is);
                                        }
                                    }
                                    catch (IOException e)
                                    {
                                        ok = false;
                                    }
                                }
                            }

                            // Replace socket.
                            if (resetHost)
                            {
                                try
                                {
                                    Thread.sleep(4200);
                                }
                                catch (InterruptedException ignored)
                                {
                                }
                            }

                            synchronized (_lockObject)
                            {
                                try
                                {
                                    socket.close();
                                }
                                catch (IOException ignored)
                                {
                                }

                                currentId = _id;

                                if (resetHost)
                                {
                                    currentHost = _relayHost;
                                }

                                if (id.equals(currentId))
                                {
                                    Log.i(TAG, "Creating new socket");

                                    _txChatSockets.remove(socket);
                                    socket = SocketHelpers.createSocket(_sslSocketFactory, SOCKET_TIMEOUT);
                                    _txChatSockets.add(socket);
                                }
                            }
                        }
                    }
                    catch (Throwable t)
                    {
                        Log.e(TAG, "Uncaught exception in startTx()", t);
                        throw new RuntimeException(t);
                    }
                }
            }).start();
        }
    }

    private Message getMessage(String id)
    {
        Log.d(TAG, String.format("getMessage(%s) pre txLock", id));
        synchronized (_txLockObject) // ok to take _txLockObject: _lockObject is not taken by caller of getMessage
        {
            Log.d(TAG, String.format("getMessage(%s) in txLock", id));
            Message result = null;

            String currentId;
            Log.d(TAG, String.format("getMessage(%s) pre Lock", id));
            synchronized (_lockObject)
            {
                Log.d(TAG, String.format("getMessage(%s) in Lock", id));
                currentId = _id;
            }

            while ((result == null) && id.equals(currentId))
            {
                synchronized (_lockObject)
                {
                    currentId = _id;

                    if (id.equals(currentId))
                    {
                        if (!_pendingSends.isEmpty())
                        {
                            ChatMessage message = _pendingSends.poll();
                            result = new SmartReplyMessage(message);
                        }
                        else if (_pendingAudiologistInfo)
                        {
                            result = new UserInfoRequestMessage();
                        }
                    }
                }

                if (id.equals(currentId) && (result == null))
                {
                    try
                    {
                        _txLockObject.wait();
                    }
                    catch (InterruptedException ignored)
                    {
                    }
                }
            }

            return result;
        }
    }

    private void startListening(final YalerSSLServerSocket listener, final String id)
    {
        Log.i(TAG, "startListening");

        synchronized (_lockObject)
        {
            new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        String currentId;
                        synchronized (_lockObject)
                        {
                            currentId = _id;
                        }

                        while (id.equals(currentId))
                        {
                            Socket s;
                            try
                            {
                                s = listener.accept(new AcceptCallback()
                                {
                                    @Override
                                    public void statusChanged(AcceptCallbackState state)
                                    {
                                        Log.v(TAG, String.format("%x %s", listener.hashCode(), state.toString()));
                                    }
                                });
                            }
                            catch (IOException e)
                            {
                                s = null;
                            }

                            synchronized (_lockObject)
                            {
                                currentId = _id;

                                if (s != null)
                                {
                                    if (id.equals(currentId))
                                    {
                                        startRx(s, id);
                                    }
                                    else
                                    {
                                        try
                                        {
                                            s.close();
                                        }
                                        catch (IOException ignored)
                                        {
                                        }
                                    }
                                }
                            }
                        }
                    }
                    catch (Throwable t)
                    {
                        Log.e(TAG, "Uncaught exception in startListening()", t);
                        throw new RuntimeException(t);
                    }
                }
            }).start();
        }
    }

    private void startRx(final Socket s, final String id)
    {
        Log.i(TAG, "startRx");

        synchronized (_lockObject)
        {
            boolean success = _rxChatSockets.add(s);
            Contract.check(success);

            new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        boolean ok = true;

                        String currentId;
                        synchronized (_lockObject)
                        {
                            currentId = _id;
                        }

                        while (ok && id.equals(currentId))
                        {
                            Message message = null;
                            try
                            {
                                InputStream stream = s.getInputStream();
                                boolean continueExpected = StreamHelpers.consumeHttpHeaders(stream);
                                if (continueExpected) {
                                    byte[] continueRspBytes =
                                        StringHelpers.convertToBytes(
                                            "HTTP/1.1 100 Continue\r\nContent-Length: 0\r\n\r\n");
                                    try
                                    {
                                        s.getOutputStream().write(continueRspBytes);
                                        s.getOutputStream().flush();
                                        Log.v(TAG, "Sent 100 Continue response");
                                    }
                                    catch (IOException e)
                                    {
                                        Log.w(TAG, "IOException while writing chat continue response.", e);
                                        ok = false;
                                    }
                                }
                                if (ok)
                                {
                                    message = MessageReader.getInstance().parse(stream);
                                }
                            }
                            catch (IOException e)
                            {
                                Log.w(TAG, "IOException while parsing chat message.", e);
                                ok = false;
                            }
                            catch (XmlPullParserException e)
                            {
                                Log.w(TAG, "XmlPullParserException while parsing chat message", e);
                                ok = false;
                            }
                            catch (MessageReaderException e)
                            {
                                Log.w(TAG, String.format("MessageReaderException while parsing chat message: %s", e.getMessage()));
                                ok = false;
                            }

                            synchronized (_lockObject)
                            {
                                currentId = _id;

                                if (message != null)
                                {
                                    if (id.equals(currentId))
                                    {
                                        handleMessage(message);
                                    }
                                }
                            }

                            byte[] rspBytes = StringHelpers.convertToBytes("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");

                            try
                            {
                                s.getOutputStream().write(rspBytes);
                            }
                            catch (IOException e)
                            {
                                Log.w(TAG, "IOException while writing chat response.", e);
                                ok = false;
                            }

                            synchronized (_lockObject)
                            {
                                currentId = _id;
                            }
                        }

                        synchronized (_lockObject)
                        {
                            try
                            {
                                s.close();
                            }
                            catch (IOException e)
                            {
                                Log.w(TAG, "IOException while closing chat socket.", e);
                            }
                            _rxChatSockets.remove(s);
                        }
                    }
                    catch (Throwable t)
                    {
                        Log.e(TAG, "Uncaught exception in startRx()", t);
                        throw new RuntimeException(t);
                    }
                }
            }).start();
        }
    }

    private void handleMessage(Message message)
    {
        synchronized (_lockObject)
        {
            Contract.check(message != null);

            Log.i(TAG, String.format("Retrieved message: %s", message));

            if (message.getType() == MessageType.USER_INFO)
            {
                _pendingAudiologistInfo = false;
                UserInfoMessage m = (UserInfoMessage)message;

                _audiologistInfo = new AudiologistInfo(m.getName(), m.getImage());
            }
            else if (message.getType() == MessageType.HI_STATUS)
            {
                HiStatusMessage m = (HiStatusMessage)message;

                HiMuteStatus newLeft = m.getMuteStatus(HiSide.LEFT);
                HiMuteStatus newRight = m.getMuteStatus(HiSide.RIGHT);

                if ((_muteStatusLeft != newLeft) && (_muteStatusRight != newRight) && (newLeft == newRight))
                {
                    if (newLeft == HiMuteStatus.MUTED)
                    {
                        addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_MUTED_BOTH));
                    }
                    else if (newLeft == HiMuteStatus.UNMUTED)
                    {
                        addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_UNMUTED_BOTH));
                    }
                }
                else
                {
                    if (_muteStatusLeft != newLeft)
                    {
                        if (newLeft == HiMuteStatus.MUTED)
                        {
                            addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_MUTED_LEFT));
                        }
                        else if (newLeft == HiMuteStatus.UNMUTED)
                        {
                            addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_UNMUTED_LEFT));
                        }
                    }
                    if (_muteStatusRight != newRight)
                    {
                        if (newRight == HiMuteStatus.MUTED)
                        {
                            addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_MUTED_RIGHT));
                        }
                        else if (newRight == HiMuteStatus.UNMUTED)
                        {
                            addMessage(new ChatMessage(ChatMessageSource.SYSTEM, ChatMessage.TEXT_HI_UNMUTED_RIGHT));
                        }
                    }
                }

                _muteStatusLeft = newLeft;
                _muteStatusRight = newRight;
            }
            else if (message.getType() == MessageType.SMART)
            {
                SmartMessage m = (SmartMessage)message;

                addMessage(new ChatMessage(ChatMessageSource.AUDIOLOGIST, m));

                _isTyping = false;
            }
            else if (message.getType() == MessageType.TYPING)
            {
                TypingMessage m = (TypingMessage)message;

                _isTyping = m.isTyping();
            }
        }
        _delegate.chatManagerStateChanged();
    }

    public void addMessage(ChatMessage message)
    {
        synchronized (_lockObject)
        {
            if (message == null)
            {
                throw new IllegalArgumentException("message is null.");
            }

            _chatHistory.add(message);
        }
        _delegate.chatManagerStateChanged();
    }

    private void requestAudiologistInfo()
    {
        synchronized (_txLockObject) { // ok to take _txLockObject: _lockObject is already taken by caller.
            synchronized (_lockObject) {
                if (AudiologistInfo.EMPTY.equals(_audiologistInfo)) {

                    _pendingAudiologistInfo = true;
                    _txLockObject.notifyAll();
                }
            }
        }
    }

    public void shutdown() {
        synchronized (_txLockObject) { // ok to take _txLockObject: _lockObject is not taken by caller shutdown
            synchronized (_lockObject) {
                _id = null;
                _relayHost = null;
                _audiologistInfo = AudiologistInfo.EMPTY;
                _muteStatusLeft = HiMuteStatus.UNDEFINED;
                _muteStatusRight = HiMuteStatus.UNDEFINED;
                _chatHistory.clear();
                _isTyping = false;

                closeSockets();


                _pendingSends.clear();
                _pendingAudiologistInfo = true;
                _txLockObject.notifyAll();
            }
        }
    }

    public AudiologistInfo getAudiologistInfo()
    {
        synchronized (_lockObject)
        {
            return _audiologistInfo;
        }
    }

    public HiMuteStatus getMuteStatus(HiSide side)
    {
        synchronized (_lockObject)
        {
            HiMuteStatus result = HiMuteStatus.UNDEFINED;
            if (side == HiSide.LEFT)
            {
                result = _muteStatusLeft;
            }
            else if (side == HiSide.RIGHT)
            {
                result = _muteStatusRight;
            }
            return result;
        }
    }

    // Cannot decrease, except when fitting session is stopped where it is reset to 0.
    public int getChatMessageCount()
    {
        synchronized (_lockObject)
        {
            return _chatHistory.size();
        }
    }

    public ChatMessage getChatMessage(int index)
    {
        synchronized (_lockObject)
        {
            return _chatHistory.get(index);
        }
    }

    // Volatile value.
    public boolean isTyping()
    {
        synchronized (_lockObject)
        {
            return _isTyping;
        }
    }

    public void sendChatMessage(ChatMessage message)
    {
        Log.d(TAG, "sendChatMessage: pre lock");
        synchronized (_txLockObject) { // ok to take _txLockObject: _lockObject is not taken by caller of sendChatMessage.
            synchronized (_lockObject) {
                Log.d(TAG, "sendChatMessage: post lock");

                if (message == null) {
                    throw new IllegalArgumentException("message is null.");
                }

                if ((message.getText() != null) && !_chatMessagePattern.matcher(message.getText()).matches()) {
                    // The Android XML Serialization library does not support characters like emoji.
                    // See line 121-137 of https://android.googlesource.com/platform/libcore/+/9edf43dfcc35c761d97eb9156ac4254152ddbc55/xml/src/main/java/org/kxml2/io/KXmlSerializer.java                    
                    throw new IllegalArgumentException("message text contains invalid characters.");
                }

                addMessage(message);

                _pendingSends.add(message);
                _txLockObject.notifyAll();

                Log.i(TAG, "Chat message enqueued: " + message);
            }
        }
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/fittingconnection/FittingConnectionManager.java
public interface FittingConnectionManager
{
    void start();

    void stop();
}
Difian/DifianApp/src/main/java/com/sonova/difian/ui/MainActivity.java
public final class MainActivity extends Activity implements FittingServiceConnectionFragmentCallback
{
    public static final String ACTION_SET_UP_FITTING_DEVICE = "com.sonova.difian.ui.mainactivity.action.SET_UP_FITTING_DEVICE";
    private static final String TAG = MainActivity.class.getName();
    private static final String UI_LOCKING_FRAGMENT_TAG = TAG + ">0";
    private static final String FITTING_SERVICE_CONNECTION_FRAGMENT_TAG = TAG + ">1";
    private static final String HOCKEY_KEY = "YOUR_HOCKEY_KEY"; // TODO
    private UiLockingFragment _uiLocking;
    private FittingServiceConnectionFragment _fittingServiceConnection;

    @Override
    public void onFittingServiceStateUpdated(FittingConnectionState state, FittingConnectionError error, String id, String audiologistName, Bitmap audiologistPicture, HiMuteStatus muteStatusLeft, HiMuteStatus muteStatusRight)
    {
        FittingBinder b = _fittingServiceConnection.getBinder();
        if (b != null)
        {
            if (state != FittingConnectionState.NOT_CONNECTED) {
                startSession(null);
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.com_sonova_difian_ui_mainactivity);
        _uiLocking = ActivityHelpers.attach(this, UiLockingFragment.class, UI_LOCKING_FRAGMENT_TAG);
        _fittingServiceConnection = ActivityHelpers.attach(this, FittingServiceConnectionFragment.class, FITTING_SERVICE_CONNECTION_FRAGMENT_TAG);

        PreferenceManager.setDefaultValues(getApplicationContext(), R.xml.preferences, false);
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        if (ACTION_SET_UP_FITTING_DEVICE.equals(getIntent().getAction()))
        {
            SharedPreferences.Editor editor = preferences.edit();
            BluetoothDevice device = getIntent().getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String id = getIntent().getStringExtra(BluetoothDevice.EXTRA_NAME);
            editor.putString(FittingInfoHelpers.FITTING_DEVICE_ADDRESS_KEY, device.getAddress());
            editor.putString(FittingInfoHelpers.FITTING_DEVICE_ID_KEY, id);
            editor.apply();
        }
        FittingInfoHelpers.updateFittingDeviceIdAndRegion(this);
        checkForUpdates();
    }

    @Override
    protected void onResume()
    {
        super.onResume();
        if (_uiLocking.isUiLocked())
        {
            _uiLocking.unlockUi();
        }

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        String id = preferences.getString(FittingInfoHelpers.FITTING_DEVICE_ID_KEY, null);
        if (id == null)
        {
            startActivity(new Intent(this, SetupMainActivity.class));
        }
        else
        {
            FittingInfoHelpers.updateFittingDeviceIdAndRegion(this);
        }
        checkForCrashes();
    }

    @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion")
    private void checkForCrashes()
    {
        CrashManagerListener listener = new CrashManagerListener()
        {
            public String getStringForResource(int resourceID)
            {
                switch (resourceID)
                {
                    case Strings.CRASH_DIALOG_TITLE_ID:
                        return getResources().getString(R.string.crash_dialog_title);
                    case Strings.CRASH_DIALOG_MESSAGE_ID:
                        return getResources().getString(R.string.crash_dialog_message);
                    case Strings.CRASH_DIALOG_NEGATIVE_BUTTON_ID:
                        return getResources().getString(R.string.crash_dialog_negative_button);
                    case Strings.CRASH_DIALOG_POSITIVE_BUTTON_ID:
                        return getResources().getString(R.string.crash_dialog_positive_button);
                    default:
                        return null;
                }
            }
        };
        CrashManager.register(this, HOCKEY_KEY, listener);
    }

    @Override
    public void onDestroy()
    {
        UpdateManager.unregister();
        super.onDestroy();
    }

    @SuppressWarnings("RefusedBequest")
    @Override
    public void onBackPressed()
    {
        if (!_uiLocking.isUiLocked())
        {
            _uiLocking.lockUi();
            finish();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.actions_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    public void startSession (View view) {
        if (!_uiLocking.isUiLocked())
        {
            _uiLocking.lockUi();
            startActivity(new Intent(this, FittingPowerOnActivity.class));
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        boolean result = false;
        if (item.getItemId() == R.id.testConnection)
        {
            if (!_uiLocking.isUiLocked())
            {
                _uiLocking.lockUi();
                startActivity(new Intent(this, TestRunningActivity.class));
            }
        }
        else if (item.getItemId() == R.id.changeFittingDevice)
        {
            if (!_uiLocking.isUiLocked())
            {
                _uiLocking.lockUi();
                startActivity(new Intent(this, SetupPowerOnActivity.class));
            }
            result = true;
        }
        else if (item.getItemId() == R.id.settings)
        {
            if (!_uiLocking.isUiLocked())
            {
                _uiLocking.lockUi();
                startActivity(new Intent(this, SettingsActivity.class));
            }
            result = true;
        }
        else
        {
            result = super.onOptionsItemSelected(item);
        }
        return result;
    }

    @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion")
    private void checkForUpdates()
    {
        // Remove this for store builds!
        UpdateManager.register(this, HOCKEY_KEY);
    }

    private void visualizeFittingState()
    {
        _fittingServiceConnection.requestUpdate();
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/chat/ChatMessage.java
public final class ChatMessage
{
    public static final String TEXT_CONNECTED = ChatMessage.class.getName() + ".CONNECTED";
    public static final String TEXT_DISCONNECTED = ChatMessage.class.getName() + ".DISCONNECTED";
    public static final String TEXT_HI_MUTED_LEFT = ChatMessage.class.getName() + ".HI_MUTED_LEFT";
    public static final String TEXT_HI_MUTED_RIGHT = ChatMessage.class.getName() + ".HI_MUTED_RIGHT";
    public static final String TEXT_HI_MUTED_BOTH = ChatMessage.class.getName() + ".HI_MUTED_BOTH";
    public static final String TEXT_HI_UNMUTED_LEFT = ChatMessage.class.getName() + ".HI_UNMUTED_LEFT";
    public static final String TEXT_HI_UNMUTED_RIGHT = ChatMessage.class.getName() + ".HI_UNMUTED_RIGHT";
    public static final String TEXT_HI_UNMUTED_BOTH = ChatMessage.class.getName() + ".HI_UNMUTED_BOTH";
    private final ChatMessageSource _source;
    private final String _text;
    private final Bitmap _image;
    private final SmartMessage _message;
    private final List<SmartMessageOption> _selection;
    private volatile boolean _acked;
    private volatile int _retries;
    private volatile boolean _aborted;

    public ChatMessage(ChatMessageSource source, String text)
    {
        Contract.check((source == ChatMessageSource.SYSTEM) || (source == ChatMessageSource.USER));

        _source = source;
        _text = text;
        _image = null;
        _message = null;
        _selection = null;
    }

    public ChatMessage(ChatMessageSource source, SmartMessage message)
    {
        Contract.check(source == ChatMessageSource.AUDIOLOGIST);

        _source = source;
        _text = message.getText();
        _image = ImageHelpers.imageBase64ToBitmap(message.getImage());
        _message = message;
        _selection = null;
    }

    public ChatMessage(ChatMessageSource source, SmartMessage message, List<SmartMessageOption> selection)
    {
        Contract.check(source == ChatMessageSource.USER);

        _source = source;
        _text = null;
        _image = null;
        _message = message;
        //noinspection AssignmentToCollectionOrArrayFieldFromParameter
        _selection = selection;
    }

    public ChatMessage(ChatMessage message)
    {
        if (message == null)
        {
            throw new IllegalArgumentException("message is null.");
        }

        _source = message.getSource();
        _text = message.getText();
        _image = message.getImage();
        _message = message.getMessage();
        if (message.getSelection() != null)
        {
            _selection = new ArrayList<SmartMessageOption>(message.getSelection());
        }
        else
        {
            _selection = null;
        }
    }

    public List<SmartMessageOption> getSelection()
    {
        List<SmartMessageOption> result = null;
        if (_selection != null)
        {
            result = Collections.unmodifiableList(_selection);
        }
        return result;
    }

    public SmartMessage getMessage()
    {
        return _message;
    }

    public Bitmap getImage()
    {
        return _image;
    }

    public String getText()
    {
        return _text;
    }

    public ChatMessageSource getSource()
    {
        return _source;
    }

    public void ack()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot be acked.");
        }

        _acked = true;
    }

    /**
     * Volatile value. Can change only form false to true.
     */
    public boolean isAcked()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot be acked.");
        }

        return _acked;
    }

    public void retry()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot be retried.");
        }
        if (isFailed())
        {
            throw new IllegalStateException("Failed messages cannot be retried.");
        }

        //noinspection NonAtomicOperationOnVolatileField
        _retries++;
    }

    /**
     * Volatile value. Can change only form false to true.
     */
    public boolean isFailed()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot fail.");
        }

        return _retries > 3;
    }

    public void abort()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot be aborted.");
        }
        if (!isFailed())
        {
            throw new IllegalStateException("Only failed messages can be aborted.");
        }
        if (isAborted())
        {
            throw new IllegalStateException("Messages can only be aborted once.");
        }

        _aborted = true;
    }

    /**
     * Volatile value. Can change only form false to true.
     */
    public boolean isAborted()
    {
        if ((getSource() == ChatMessageSource.AUDIOLOGIST) || (getSource() == ChatMessageSource.SYSTEM))
        {
            throw new IllegalStateException("Message cannot be aborted.");
        }

        return _aborted;
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/messaging/HiMuteStatus.java
public enum HiMuteStatus
{
    MUTED, UNMUTED, UNDEFINED
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/chat/ChatMessageSource.java
public enum ChatMessageSource
{
    AUDIOLOGIST,
    USER,
    SYSTEM
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/fittingconnection/FittingConnectionManagerFactory.java
public interface FittingConnectionManagerFactory
{
    FittingConnectionManager createFittingConnectionManager(String serialAddress, String relayHost, FittingConnectionManagerCallback listener);
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/chat/AudiologistInfo.java
public final class AudiologistInfo
{
    private final String _audiologistName;
    private final Bitmap _audiologistPicture;

    public static final AudiologistInfo EMPTY = new AudiologistInfo();

    private AudiologistInfo()
    {
        _audiologistName = null;
        _audiologistPicture = null;
    }

    /**
     * @param picture Base 64 image from Smart message.
     */
    public AudiologistInfo(String name, String picture)
    {
        _audiologistName = name;
        _audiologistPicture = ImageHelpers.imageBase64ToBitmap(picture);
    }

    public String getName()
    {
        return _audiologistName;
    }

    public Bitmap getPicture()
    {
        return _audiologistPicture;
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/fittingconnection/FittingConnectionManagerState.java
public final class FittingConnectionManagerState
{
    private final FittingConnectionState _connectionState;
    private final FittingConnectionError _connectionError;
    private final String _id;

    public static final FittingConnectionManagerState EMPTY = new FittingConnectionManagerState(FittingConnectionState.NOT_CONNECTED, FittingConnectionError.NONE, null);

    public FittingConnectionManagerState(FittingConnectionState connectionState, FittingConnectionError connectionError, String id)
    {
        _connectionState = connectionState;
        _connectionError = connectionError;
        _id = id;
    }

    public FittingConnectionState getConnectionState()
    {
        return _connectionState;
    }

    public FittingConnectionError getConnectionError()
    {
        return _connectionError;
    }

    public String getId()
    {
        return _id;
    }
}
Difian/DifianApp/src/main/java/com/sonova/difian/communication/messaging/HiSide.java
public enum HiSide
{
    LEFT, RIGHT
}


Question: import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import com.sonova.difian.R;
import com.sonova.difian.communication.chat.AudiologistInfo;
import com.sonova.difian.communication.chat.ChatManager;
import com.sonova.difian.communication.chat.ChatManagerCallback;
import com.sonova.difian.communication.chat.ChatMessage;
import com.sonova.difian.communication.chat.ChatMessageSource;
import com.sonova.difian.communication.fittingconnection.FittingConnectionManager;
import com.sonova.difian.communication.fittingconnection.FittingConnectionManagerCallback;
import com.sonova.difian.communication.fittingconnection.FittingConnectionManagerFactory;
import com.sonova.difian.communication.fittingconnection.FittingConnectionManagerState;
import com.sonova.difian.communication.fittingconnection.FittingConnectionState;
import com.sonova.difian.communication.messaging.HiMuteStatus;
import com.sonova.difian.communication.messaging.HiSide;
import com.sonova.difian.communication.timer.SessionTimer;
import com.sonova.difian.ui.MainActivity;
                    }
                }

                broadcastState();
            }
        }
    };

    public FittingBinder(Service service, FittingConnectionManagerFactory fittingConnectionManagerFactory)
    {
        if (fittingConnectionManagerFactory == null)
        {
            throw new IllegalArgumentException("fittingConnectionManagerFactory is null");
        }
        _fittingConnectionManagerFactory = fittingConnectionManagerFactory;

        _service = service;
        _chat = new ChatManager(_chatListener);
    }

    public void setFittingDeviceAddress(String address)
    {
        synchronized (_lockObject)
        {
            if (_state.getConnectionState() != FittingConnectionState.NOT_CONNECTED)
            {
                throw new IllegalStateException("Fitting device address cannot be reconfigured while fitting session running. Wait for NOT_CONNECTED state.");
            }
            _deviceAddress = address;
        }
    }

    public void setRelayConfiguration(String host)
    {
        synchronized (_lockObject)
        {
            if (_state.getConnectionState() != FittingConnectionState.NOT_CONNECTED)
            {
                throw new IllegalStateException("Relay cannot be reconfigured while fitting session is running. Wait for NOT_CONNECTED state.");
            }
            _relayHost = host;
        }
    }

    public void start()
    {
        synchronized (_lockObject)
        {
            if (_deviceAddress == null)
            {
                throw new IllegalStateException("setFittingDeviceAddress was not called prior to starting the session.");
            }

            if (_state.getConnectionState() != FittingConnectionState.NOT_CONNECTED)
            {
                throw new IllegalStateException("A fitting session is already running. Wait for NOT_CONNECTED state.");
            }

            Intent intent = new Intent(_service, MainActivity.class);
            intent.setAction(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_LAUNCHER);

            PendingIntent pi = PendingIntent.getActivity(_service, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

            String title = _service.getString(R.string.notification_fittingservice_title);
            String contentText = _service.getString(R.string.notification_fittingservice_text);

            Notification n = new Notification.Builder(_service).setSmallIcon(R.drawable.icon_bw).setContentTitle(title).setContentText(contentText).setContentIntent(pi).setOngoing(true).build();

            //noinspection ProhibitedExceptionCaught
            try
            {
                _service.startForeground(ONGOING_NOTIFICATION, n);
            }
            catch (NullPointerException ignored)
            {
                // WORKAROUND Occurs during testing, as startForeground is unavailable.
                // See https://code.google.com/p/android/issues/detail?id=12122
            }

            _fittingConnection = _fittingConnectionManagerFactory.createFittingConnectionManager(_deviceAddress, _relayHost, _listener);
            _fittingConnection.start();
        }
    }

    public void stop()
    {
        synchronized (_lockObject)
        {
            if (_fittingConnection != null)
            {
                _fittingConnection.stop();

                //noinspection ProhibitedExceptionCaught
                try
                {
                    _service.stopForeground(true);
                }
                catch (NullPointerException ignored)
                {
                    // WORKAROUND Occurs during testing, as stopForeground is unavailable.
                    // See https://code.google.com/p/android/issues/detail?id=12122
                }
            }
        }
    }

    public FittingConnectionManagerState getState()
    {
        synchronized (_lockObject)
        {
            return _state;
        }
    }

    public AudiologistInfo getAudiologistInfo()
    {
        return _chat.getAudiologistInfo();
    }

Answer:     public HiMuteStatus getMuteStatus(HiSide side)