//
//  Copyright (c) 2019 Rally Tactical Systems, Inc.
//  All rights reserved.
//

package com.rallytac.engage.engine;

import android.annotation.SuppressLint;
import androidx.annotation.Keep;

import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;

import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.NoiseSuppressor;
import android.media.MediaRecorder;

import org.json.JSONObject;
import org.json.JSONArray;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;
import java.util.List;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.NetworkInterface;
import java.util.Enumeration;
import android.util.Log;
import java.util.Random;

import android.util.Base64;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.io.IOException;

@Keep
public final class Engine
{
     // Engage engine library interface
    static
    {
        System.loadLibrary("engage-shared");
    }

    public interface IAudioProvider
    {
        public AudioManager init();
        public void deInit();

        public AudioManager getManager();
        public AudioRecord getRecorder();
        public AudioTrack getTrack();
        public NoiseSuppressor getAudioNoiseSuppressor();
        public AcousticEchoCanceler getAec();

        public int createAudioSubsystem(String jsonParams);
        public int destroyAudioSubsystem(String jsonParams);

        public int startAudioRecording(String jsonParams);
        public int stopAudioRecording(String jsonParams);

        public int startAudioPlayout(String jsonParams);
        public int stopAudioPlayout(String jsonParams);        
    }

    private static int getRandomInt(int min, int max)
    {
        return (new Random()).nextInt((max - min) + 1) + min;
    }

    private Audio _audio = null;
    private native void engageInternalWriteAndroidAudio(short[] buffer, int ofs, int len);
    private native int engageInternalReadAndroidAudio(short[] buffer, int ofs, int len);

    public void writeAndroidAudio(short[] buffer, int ofs, int len)
    {
        engageInternalWriteAndroidAudio(buffer, ofs, len);
    }

    public int readAndroidAudio(short[] buffer, int ofs, int len)
    {
        return engageInternalReadAndroidAudio(buffer, ofs, len);
    }

    public Audio getAudio()
    {
        return _audio;
    }

    @Keep
    private Audio onCreateAudio(String jsonParams)
    {
        if(_audio == null)
        {
            _audio = new Audio();
            _audio.init();
            _audio.createAudioSubsystem(jsonParams);
        }

        return _audio;
    }

    @Keep
    private void onDestroyAudio()
    {
        if(_audio != null)
        {
            _audio.destroyAudioSubsystem(null);
            _audio.deInit();
            _audio = null;
        }
    }

    @Keep
    public final class Audio
    {
        private AudioManager _manager = null;
        private AudioRecord _rec = null;
        private AudioTrack _track = null;
        private NoiseSuppressor _noiseSuppressor = null;
        private AcousticEchoCanceler _aec = null;
        private RecordingThread _recordingThread = null;
        private PlayoutThread _playoutThread = null;
        private int _recMinBufferSize = -1;

        private Audio()
        {
        }

        public void init()
        {
            if(_audioProvider != null)
            {
                _manager = _audioProvider.init();
            }
            else
            {
                if(_audioManager == null)
                {
                    _manager = (AudioManager) _appContext.getSystemService(Context.AUDIO_SERVICE);
                }
                else
                {
                    _manager = _audioManager;
                }
            }
        }

        public void deInit()
        {
            if(_audioProvider != null)
            {
                _audioProvider.deInit();
            }

            _manager = null;
        }

        public AudioManager getManager()
        {
            if(_audioProvider != null)
            {
                return _audioProvider.getManager();
            }
            else
            {
                return _manager;
            }
        }

        public AudioRecord getRecorder()
        {
            if(_audioProvider != null)
            {
                return _audioProvider.getRecorder();
            }
            else
            {
                return _rec;
            }
        }

        public AudioTrack getTrack()
        {
            if(_audioProvider != null)
            {
                return _audioProvider.getTrack();
            }
            else
            {
                return null;
            }
        }

        public NoiseSuppressor getAudioNoiseSuppressor()
        {
            if(_audioProvider != null)
            {
                return _audioProvider.getAudioNoiseSuppressor();
            }
            else
            {
                return _noiseSuppressor;
            }
        }

        public AcousticEchoCanceler getAec()
        {
            if(_audioProvider != null)
            {
                return _audioProvider.getAec();
            }
            else
            {
                return _aec;
            }
        }

        
        static private final int AS_SAMPLE_RATE = 8000;
        static private final int AS_CHANNELS = 1;
        static private final int AS_USAGE = AudioAttributes.USAGE_VOICE_COMMUNICATION;
        static private final int AS_CONTENT_TYPE = AudioAttributes.CONTENT_TYPE_SPEECH;
        static private final int AS_SOURCE = MediaRecorder.AudioSource.MIC;

        @SuppressLint("MissingPermission")
        @Keep
        private int createAudioSubsystem(String jsonParams)
        {
            int rc = -1;

            if(_audioProvider != null)
            {
                rc = _audioProvider.createAudioSubsystem(jsonParams);
            }
            else
            {
                try
                {
                    //int source = MediaRecorder.AudioSource.VOICE_COMMUNICATION;                    
                    boolean useNs = true;
                    boolean useAec = true;

                    if(_rec == null)
                    {
                        _recMinBufferSize = AudioRecord.getMinBufferSize(
                                AS_SAMPLE_RATE,
                                (AS_CHANNELS == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO),
                                AudioFormat.ENCODING_PCM_16BIT);

                        _rec = new AudioRecord(AS_SOURCE,
                                AS_SAMPLE_RATE,
                                (AS_CHANNELS == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO),
                                AudioFormat.ENCODING_PCM_16BIT,
                                _recMinBufferSize);

                        if(_rec == null)
                        {
                            throw new Exception("failed to create audio recorder");
                        }

                        if(useNs)
                        {
                            if(NoiseSuppressor.isAvailable())
                            {
                                _noiseSuppressor = NoiseSuppressor.create(_rec.getAudioSessionId());
                                if(_noiseSuppressor == null)
                                {
                                    shimLayerInternalLogging_D(TAG, "cannot create noise suppressor for audio recording");
                                }
                                else
                                {
                                    _noiseSuppressor.setEnabled(true);
                                    if(!_noiseSuppressor.getEnabled())
                                    {
                                        shimLayerInternalLogging_D(TAG, "cannot enable noise suppressor for audio recording");
                                    }
                                }
                            }
                        }

                        if(useAec)
                        {
                            if(AcousticEchoCanceler.isAvailable())
                            {
                                _aec = AcousticEchoCanceler.create(_rec.getAudioSessionId());
                                if(_aec == null)
                                {
                                    shimLayerInternalLogging_D(TAG, "cannot create aec for audio recording");
                                }
                                else
                                {
                                    _aec.setEnabled(true);
                                    if(!_aec.getEnabled())
                                    {
                                        shimLayerInternalLogging_D(TAG, "cannot enable aec for audio recording");
                                    }
                                }
                            }
                        }

                        rc = 0;
                    }
                    else
                    {
                        rc = 0;
                    }
                }
                catch (Exception e)
                {
                    shimLayerInternalLogging_E(TAG, e.getMessage());
                    rc = -1;
                }
            }

            return rc;
        }

        @Keep
        private int destroyAudioSubsystem(String jsonParams)
        {
            int rc = 0;

            if(_audioProvider != null)
            {
                rc = _audioProvider.destroyAudioSubsystem(jsonParams);
            }
            else
            {
                if(_noiseSuppressor != null)
                {
                    _noiseSuppressor.release();
                    _noiseSuppressor = null;
                }

                if(_aec != null)
                {
                    _aec.release();
                    _aec = null;
                }

                if(_rec != null)
                {
                    _rec.release();
                    _rec = null;
                }
            }

            return rc;
        }

        @Keep
        private int onStartAudioRecording(String jsonParams)
        {
            int rc = 0;

            if(_audioProvider != null)
            {
                rc = _audioProvider.startAudioRecording(jsonParams);
            }
            else
            {
                if(_rec != null && _recordingThread == null)
                {
                    _recordingThread = new RecordingThread(_rec, _recMinBufferSize);
                    _recordingThread.start();
                }
            }

            return rc;
        }

        @Keep
        private int onStopAudioRecording(String jsonParams)
        {
            int rc = 0;

            if(_audioProvider != null)
            {
                rc = _audioProvider.stopAudioRecording(jsonParams);
            }
            else
            {
                if(_recordingThread != null)
                {
                    _recordingThread.close();
                    _recordingThread = null;
                }
            }

            return rc;
        }

        @Keep
        private int onStartAudioPlayout(String jsonParams)
        {
            int rc = 0;

            if(_audioProvider != null)
            {
                rc = _audioProvider.startAudioPlayout(jsonParams);
            }
            else
            {
                if(_playoutThread == null)
                {
                    _playoutThread = new PlayoutThread();
                    _playoutThread.start();
                }
            }

            return rc;
        }

        @Keep
        private int onStopAudioPlayout(String jsonParams)
        {
            int rc = 0;

            if(_audioProvider != null)
            {
                rc = _audioProvider.stopAudioPlayout(jsonParams);
            }
            else
            {
                if(_playoutThread != null)
                {
                    _playoutThread.close();
                    _playoutThread = null;
                }
            }

            return rc;
        }

        // The recording thread
        private class RecordingThread extends Thread
        {
            private boolean _running = true;
            private AudioRecord _recorder = null;
            private int _minBufferSizeIn = 0;
            private String TAG = null;

            RecordingThread(AudioRecord recorder, int minBufferSizeIn)
            {
                TAG = RecordingThread.class.getSimpleName() + "-" + getRandomInt(1, 999999);
                shimLayerInternalLogging_D(TAG, "#DBG# created");

                _recorder = recorder;
                _minBufferSizeIn = minBufferSizeIn;
            }

            public void close()
            {
                _running = false;
                try
                {
                    join(500);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }

            public void run()
            {
                shimLayerInternalLogging_D(TAG, "#DBG# starting recording");

                short[] audioData = new short[_minBufferSizeIn];
                _recorder.startRecording();

                while( _running )
                {
                    try
                    {
                        int numRead = _recorder.read(audioData, 0, _minBufferSizeIn);
                        if(numRead > 0)
                        {
                            engageInternalWriteAndroidAudio(audioData, 0, numRead);
                        }
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }

                _recorder.stop();

                shimLayerInternalLogging_D(TAG, "#DBG# ended recording");
            }
        }

        // The playout thread
        private class PlayoutThread extends Thread
        {
            private boolean _running = true;
            private String TAG = null;
            private AudioTrack _track = null;

            PlayoutThread()
            {
                TAG = PlayoutThread.class.getSimpleName() + "-" + getRandomInt(1, 999999);
                shimLayerInternalLogging_D(TAG, "#DBG# created");
            }

            public void close()
            {
                _running = false;

                shimLayerInternalLogging_D(TAG, "#DBG# close begin");

                closeTrack();

                try
                {
                    shimLayerInternalLogging_D(TAG, "#DBG# join begin");
                    join(500);
                    shimLayerInternalLogging_D(TAG, "#DBG# join end");
                }
                catch (Exception e)
                {
                    shimLayerInternalLogging_E(TAG, "#DBG# exception:" + e.getMessage());
                    e.printStackTrace();
                }

                closeTrack();

                shimLayerInternalLogging_D(TAG, "#DBG# close end");
            }

            private void openTrack()
            {
                closeTrack();

                try
                {
                    _track = new AudioTrack.Builder()
                            .setAudioAttributes(new AudioAttributes.Builder()
                                    .setUsage(AS_USAGE)
                                    .setContentType(AS_CONTENT_TYPE)
                                    .build())
                            .setAudioFormat(new AudioFormat.Builder()
                                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                                    .setSampleRate(AS_SAMPLE_RATE)
                                    .setChannelMask((AS_CHANNELS == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO))
                                    .build())
                            .setSessionId(_rec.getAudioSessionId())
                            .setTransferMode(AudioTrack.MODE_STREAM)
                            .build();

                    if(_track == null)
                    {
                        shimLayerInternalLogging_E(TAG, "failed to create audio track");
                    }
                    else
                    {
                        _track.play();
                    }
                }
                catch( Exception e )
                {
                    shimLayerInternalLogging_E(TAG, "exception while creating track - " + e.getMessage());
                    e.printStackTrace();
                    closeTrack();
                }
            }

            private void closeTrack()
            {
                try
                {
                    if(_track != null)
                    {
                        if(_track.getState() != AudioTrack.STATE_UNINITIALIZED)
                        {
                            if(_track.getPlayState() != AudioTrack.PLAYSTATE_STOPPED)
                            {
                                _track.stop();
                            }
                        }
                    }
                }
                catch(Exception e)
                {
                    shimLayerInternalLogging_E(TAG, "exception while closing track - " + e.getMessage());
                    e.printStackTrace();
                }

                try
                {
                    if(_track != null)
                    {
                        _track.release();
                    }
                }
                catch(Exception e)
                {
                    shimLayerInternalLogging_E(TAG, "exception while releasing track - " + e.getMessage());
                    e.printStackTrace();
                }

                _track = null;
            }          

            public void run()
            {
                // We'll want 20ms of audio each time
                final int BUFSZ = ((AS_SAMPLE_RATE / 1000) * 20);
                short[] buf = new short[BUFSZ];
                int numWritten;
                int numLeft;
                int ofs;            

                shimLayerInternalLogging_D(TAG, "#DBG# starting playout");

                try
                {                                        
                    while( _running )
                    {
                        if(_track == null)
                        {
                            shimLayerInternalLogging_D(TAG, "#DBG# opening a new track");
                            openTrack();
                            shimLayerInternalLogging_D(TAG, "#DBG# after openTrack");
                        }

                        numLeft = engageInternalReadAndroidAudio(buf, 0, BUFSZ);

                        if(numLeft > 0)
                        {
                            ofs = 0;
                            while(_running && (_track != null) && (numLeft > 0))
                            {
                                /*
                                if(getRandomInt(1, 100) <= 10)
                                {
                                    shimLayerInternalLogging_I(TAG, "#DBG# ---------------------- randomly stopping track");

                                    try
                                    {
                                        _track.stop();
                                    }
                                    catch(Exception e)
                                    {                                        
                                    }
                                }
                                */

                                try
                                {
                                    numWritten = _track.write(buf, ofs, numLeft);
                                    if(numWritten <= 0)
                                    {
                                        shimLayerInternalLogging_D(TAG, "#DBG# _track.write failed with a result of " + numWritten);
                                        throw new Exception("_track.write failed with a result of " + numWritten);
                                    }

                                    numLeft -= numWritten;
                                    ofs += numWritten;
                                }
                                catch(Exception e)
                                {
                                    numLeft = 0;
                                    shimLayerInternalLogging_D(TAG, "#DBG# track write exception - closing the existing track, " + e.getMessage());
                                    closeTrack();
                                }
                            }
                        }
                        else
                        {
                            shimLayerInternalLogging_D(TAG, "#DBG# no samples available for speaker");
                            try
                            {
                                Thread.sleep(100);
                            }
                            catch (Exception e)
                            {
                                shimLayerInternalLogging_D(TAG, "#DBG# exception - " + e.getMessage());
                                e.printStackTrace();
                            }
                        }
                    }
                }
                catch(Exception e)
                {
                    shimLayerInternalLogging_D(TAG, "#DBG# exception in playout thread - " + e.getMessage());
                    e.printStackTrace();
                }

                closeTrack();

                shimLayerInternalLogging_D(TAG, "#DBG# ended playout");
            }
        }
    }

    @Keep
    public final class EngageDatagram
    {
        private byte[] _bytes = null;
        private int _status = 0;

        public byte[] getBytes()
        {
            return _bytes;
        }

        public void setBytes(byte[] bytes)
        {
            _bytes = bytes;
        }

        public void setStatus(int status)
        {
            _status = status;
        }

        public int getStatus()
        {
            return _status;
        }

        public void reset()
        {
            _bytes = null;
            _status = 0;
        }
    }

    public enum AndroidAudioApi
    {
        undefined(0),
        aaudio(1),
        opensles(2)
        ;

        private final int _val;

        private AndroidAudioApi(int val)
        {
            this._val = val;
        }

        public static AndroidAudioApi fromInt(int i)
        {
            for (AndroidAudioApi e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioApi e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }   

    public enum AndroidAudioSharingMode
    {
        exclusive(0),
        shared(1)
        ;

        private final int _val;

        private AndroidAudioSharingMode(int val)
        {
            this._val = val;
        }

        public static AndroidAudioSharingMode fromInt(int i)
        {
            for (AndroidAudioSharingMode e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioSharingMode e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }    

    public enum AndroidAudioPerformanceMode
    {
        none(10),
        powerSaving(11),
        lowLatency(11)
        ;

        private final int _val;

        private AndroidAudioPerformanceMode(int val)
        {
            this._val = val;
        }

        public static AndroidAudioPerformanceMode fromInt(int i)
        {
            for (AndroidAudioPerformanceMode e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioPerformanceMode e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }    

    public enum AndroidAudioUsage
    {
         media(1),
         voiceCommunication(2),
         voiceCommunicationSignalling(3),
         alarm(4),
         notification(5),
         notificationRingtone(6),
         notificationEvent(10),
         assistanceAccessibility(11),
         assistanceNavigationGuidance(12),
         assistanceSonification(13),
         game(14),
         assistant(15)
        ;

        private final int _val;

        private AndroidAudioUsage(int val)
        {
            this._val = val;
        }

        public static AndroidAudioUsage fromInt(int i)
        {
            for (AndroidAudioUsage e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioUsage e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    } 

    public enum AndroidAudioContentType
    {
         speech(1),
         music(2),
         movie(3),
         sonification(4)
        ;

        private final int _val;

        private AndroidAudioContentType(int val)
        {
            this._val = val;
        }

        public static AndroidAudioContentType fromInt(int i)
        {
            for (AndroidAudioContentType e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioContentType e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    } 

    public enum AndroidAudioInputPreset
    {
         generic(1),
         camcorder(5),
         voiceRecognition(6),
         voiceCommunication(7),
         unprocessed(9),
         voicePerformance(10)
        ;

        private final int _val;

        private AndroidAudioInputPreset(int val)
        {
            this._val = val;
        }

        public static AndroidAudioInputPreset fromInt(int i)
        {
            for (AndroidAudioInputPreset e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioInputPreset e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    } 

    public enum AndroidAudioEngineMode
    {
         lowlevelApi(0),
         highlevelApi(1)
        ;

        private final int _val;

        private AndroidAudioEngineMode(int val)
        {
            this._val = val;
        }

        public static AndroidAudioEngineMode fromInt(int i)
        {
            for (AndroidAudioEngineMode e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(AndroidAudioEngineMode e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum EngageResult
    {
        ok(0),
        invalidParams(-1),
        notInitialized(-2),
        alreadyInitialized(-3),
        generalFailure(-4),
        notStarted(-5),
        alreadyStarted(-6),
        insufficientDestinationSpace(-7),
        cryptoModuleInitializationFailure(-8),
        highResTimerAlreadyExists(-9)
        ;

        private final int _val;

        private EngageResult(int val)
        {
            this._val = val;
        }

        public static EngageResult fromInt(int i)
        {
            for (EngageResult e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(EngageResult e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum LoggingLevel
    {
        fatal(0),
        warning(1),
        error(2),
        information(3),
        debug(4)
        ;

        private final int _val;

        private LoggingLevel(int val)
        {
            this._val = val;
        }

        public static LoggingLevel fromInt(int i)
        {
            for (LoggingLevel e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(LoggingLevel e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum PresenceGroupItemStatusFlags
    {
        none(0),
        joined(1),
        connected(2),
        rxMuted(4),
        txMuted(8)
        ;

        private final int _val;

        private PresenceGroupItemStatusFlags(int val)
        {
            this._val = val;
        }

        public static PresenceGroupItemStatusFlags fromInt(int i)
        {
            for (PresenceGroupItemStatusFlags e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(PresenceGroupItemStatusFlags e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum JitterBufferLatency
    {
        standard(0),
        lowLatency(1)
        ;

        private final int _val;

        private JitterBufferLatency(int val)
        {
            this._val = val;
        }

        public static JitterBufferLatency fromInt(int i)
        {
            for (JitterBufferLatency e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(JitterBufferLatency e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum CreationStatus
    {
        csUndefined(0),
        csOk(1),
        csNoJson(-1),
        csConflictingRpListAndCluster(-2),
        csAlreadyExists(-3),
        csInvalidConfiguration(-4),
        csInvalidJson(-5),
        csCryptoFailure(-6),
        csAudioInputFailure(-7),
        csAudioOutputFailure(-8),
        csUnsupportedAudioEncoder(-9),
        csNoLicense(-10),
        ;

        private final int _val;

        private CreationStatus(int val)
        {
            this._val = val;
        }

        public static CreationStatus fromInt(int i)
        {
            for (CreationStatus e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(CreationStatus e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum TxStatus
    {
        undefined(0),
        started(1),
        ended(2),
        notAnAudioGroup(-1),
        notJoined(-2),
        notConnected(-3),
        alreadyTransmitting(-4),
        invalidParams(-5),
        priorityTooLow(-6),
        rxActiveOnNonFdx(-7),
        cannotSubscribeToMic(-8),
        invalidId(-9),
        txEndedWithFailure(-10),
        othersActive(-11),
        ;

        private final int _val;

        private TxStatus(int val)
        {
            this._val = val;
        }

        public static TxStatus fromInt(int i)
        {
            for (TxStatus e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(TxStatus e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum ConnectionType
    {
        undefined(0),
        ipMulticast(1),
        rallypoint(2)
        ;

        private final int _val;

        private ConnectionType(int val)
        {
            this._val = val;
        }

        public static ConnectionType fromInt(int i)
        {
            for (ConnectionType e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(ConnectionType e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum LicenseType
    {
        unknown(0),
        perpetual(1),
        expires(2)
        ;

        private final int _val;

        private LicenseType(int val)
        {
            this._val = val;
        }

        public static LicenseType fromInt(int i)
        {
            for (LicenseType e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(LicenseType e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum LicensingStatusCode
    {
        ok(0),
        nullEntitlementKey(-1),
        nullLicenseKey(-2),
        invaludLicenseKeyLen(-3),
        licenseKeyVerificationFailure(-4),
        activationCodeVerificationFailure(-5),
        invalidExpirationDate(-6),
        generalFailure(-7),
        notInitialized(-8),
        requiresActivation(-9),
        notSuitedForActivation(-10)
        ;

        private final int _val;

        private LicensingStatusCode(int val)
        {
            this._val = val;
        }

        public static LicensingStatusCode fromInt(int i)
        {
            for (LicensingStatusCode e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(LicensingStatusCode e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum NetworkDeviceFamily
    {
        ipv4(2),
        ipv6(30)
        ;

        private final int _val;

        private NetworkDeviceFamily(int val)
        {
            this._val = val;
        }

        public static NetworkDeviceFamily fromInt(int i)
        {
            for (NetworkDeviceFamily e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(NetworkDeviceFamily e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum NetworkTxPriority
    {
        priBestEffort(0),
        priSignaling(2),
        priVideo(3),
        priVoice(4),
        ;

        private final int _val;

        private NetworkTxPriority(int val)
        {
            this._val = val;
        }

        public static NetworkTxPriority fromInt(int i)
        {
            for (NetworkTxPriority e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(NetworkTxPriority e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum BlobType
    {
        undefined(0),
        appTextUtf8(1),
        jsonTextUtf8(2),
        appBinary(3),
        engageHumanBiometrics(4)
        ;

        private final int _val;

        private BlobType(int val)
        {
            this._val = val;
        }

        public static BlobType fromInt(int i)
        {
            for (BlobType e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(BlobType e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    public enum HumanBiometricsElement
    {
        undefined(0),
        heartRate(1),
        skinTemp(2),
        coreTemp(3),
        hydration(4),
        bloodOxygenation(5),
        fatigueLevel(6),
        taskEffectiveness(7)
        ;

        private final int _val;

        private HumanBiometricsElement(int val)
        {
            this._val = val;
        }

        public static HumanBiometricsElement fromInt(int i)
        {
            for (HumanBiometricsElement e : values())
            {
                if (e._val == i)
                {
                    return e;
                }
            }

            return null;
        }

        public static int toInt(HumanBiometricsElement e)
        {
            return e.toInt();
        }

        public int toInt()
        {
            return _val;
        }
    }

    // Group sources
    public static final String GROUP_SOURCE_ENGAGE_INTERNAL = "com.rallytac.engage.internal";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_CISTECH = "com.rallytac.engage.magellan.cistech";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_TRELLISWARE = "com.rallytac.engage.magellan.trellisware";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_SILVUS = "com.rallytac.engage.magellan.silvus";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_PERSISTENT = "com.rallytac.engage.magellan.persistent";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_DOMO = "com.rallytac.engage.magellan.domo";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_KENWOOD = "com.rallytac.engage.magellan.kenwood";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_TAIT = "com.rallytac.engage.magellan.tait";
    public static final String GROUP_SOURCE_ENGAGE_MAGELLAN_VOCALITY = "com.rallytac.engage.magellan.vocality";

    // Group disconnected reasons
    public static final String GROUP_DISCONNECTED_REASON_NO_REAON = "NoReason";
    public static final String GROUP_DISCONNECTED_REASON_NO_LINK = "NoLink";
    public static final String GROUP_DISCONNECTED_REASON_UNREGISTERED = "Unregistered";
    public static final String GROUP_DISCONNECTED_REASON_NOT_ALLOWED = "NotAllowed";
    public static final String GROUP_DISCONNECTED_REASON_GENERAL_DENIAL = "GeneralDenial";

    public final class JsonFields
    {
        public final class FipsCryptoSettings
        {
            public static final String objectName = "fipsCrypto";
            public static final String enabled = "enabled";
            public static final String path = "path";
        }

        public final class Tls
        {
            public static final String objectName = "tls";
            public static final String verifyPeers = "verifyPeers";
            public static final String allowSelfSignedCertificates = "allowSelfSignedCertificates";
            public static final String caCertificates = "caCertificates";
            public static final String subjectRestrictions = "subjectRestrictions";
            public static final String issuerRestrictions = "issuerRestrictions";
        }

        public final class WatchdogSettings
        {
            public static final String objectName = "watchdog";
            public static final String enabled = "enabled";
            public static final String intervalMs = "intervalMs";
            public static final String hangDetectionMs = "hangDetectionMs";
            public static final String abortOnHang = "abortOnHang";
            public static final String slowExecutionThresholdMs = "slowExecutionThresholdMs";
        }

        public final class NetworkDeviceDescriptor
        {
            public static final String objectName = "networkDeviceDescriptor";
            public static final String deviceId = "deviceId";
            public static final String name = "name";
            public static final String manufacturer = "manufacturer";
            public static final String model = "model";
            public static final String hardwareId = "hardwareId";
            public static final String serialNumber = "serialNumber";
            public static final String type = "type";
            public static final String extra = "extra";
        }

        public final class GroupCreationDetail
        {
            public static final String objectName = "groupCreationDetail";
            public static final String id = "id";
            public static final String status = "status";
        }

        public final class GroupTxDetail
        {
            public static final String objectName = "groupTxDetail";
            public static final String id = "id";
            public static final String status = "status";
            public static final String localPriority = "localPriority";
            public static final String remotePriority = "remotePriority";
            public static final String nonFdxMsHangRemaining = "nonFdxMsHangRemaining";
        }

        public final class RallypointConnectionDetail
        {
            public static final String objectName = "rallypointConnectionDetail";
            public static final String internalId = "internalId";
            public static final String host = "host";
            public static final String port = "port";
            public static final String msToNextConnectionAttempt = "msToNextConnectionAttempt";
        }

        public final class GroupConnectionDetail
        {
            public static final String objectName = "groupConnectionDetail";
            public static final String id = "id";
            public static final String connectionType = "connectionType";
            public static final String peer = "peer";
            public static final String asFailover = "asFailover";
            public static final String reason = "reason";
        }

        public final class CertStoreCertificateElement
        {
            public static final String objectName = "certStoreCertificateElement";
            public static final String arrayName = "certificates";
            public static final String id = "id";
            public static final String hasPrivateKey = "hasPrivateKey";
            public static final String tags = "tags";
        }

        public final class CertStoreDescriptor
        {
            public static final String objectName = "certStoreDescriptor";
            public static final String id = "id";
            public static final String fileName = "fileName";
            public static final String version = "version";
            public static final String flags = "flags";
            public static final String certificates = "certificates";
        }

        public final class ListOfAudioDevice
        {
            public static final String objectName = "list";
        }

        public final class AudioDevice
        {
            public static final String objectName = "audioDevice";
            public static final String deviceId = "deviceId";
            public static final String samplingRate = "samplingRate";
            public static final String msPerBuffer = "msPerBuffer";
            public static final String bufferCount = "bufferCount";
            public static final String channels = "channels";
            public static final String direction = "direction";
            public static final String boostPercentage = "boostPercentage";
            public static final String isAdad = "isAdad";
            public static final String name = "name";
            public static final String manufacturer = "manufacturer";
            public static final String model = "model";
            public static final String hardwareId = "hardwareId";
            public static final String serialNumber = "serialNumber";
            public static final String isDefault = "isDefault";
            public static final String extra = "extra";
            public static final String type = "type";
            public static final String isPresent = "isPresent";
        }

        public final class ListOfNetworkInterfaceDevice
        {
            public static final String objectName = "list";
        }

        public final class NetworkInterfaceDevice
        {
            public static final String objectName = "networkInterfaceDevice";
            public static final String name = "name";
            public static final String friendlyName = "friendlyName";
            public static final String description = "description";
            public static final String family = "family";
            public static final String address = "address";
            public static final String available = "available";
            public static final String isLoopback = "isLoopback";
            public static final String supportsMulticast = "supportsMulticast";
            public static final String hardwareAddress = "hardwareAddress";
        }

        public final class AdvancedTxParams
        {
            public static final String objectName = "advancedTxParams";
            public static final String flags = "flags";
            public static final String priority = "priority";
            public static final String subchannelTag = "subchannelTag";
            public static final String includeNodeId = "includeNodeId";
            public static final String alias = "alias";
            public static final String muted = "muted";
            public static final String txId = "txId";
            public static final String aliasSpecializer = "aliasSpecializer";
            public static final String receiverRxMuteForAliasSpecializer = "receiverRxMuteForAliasSpecializer";
            public static final String reBegin = "reBegin";

            public final class AudioUri
            {
                public static final String objectName = "audioUri";
                public static final String uri = "uri";
                public static final String repeatCount = "repeatCount";
            }
        }

        public final class License
        {
            public static final String objectName = "license";
            public static final String entitlement = "entitlement";
            public static final String key = "key";
            public static final String activationCode = "activationCode";
            public static final String deviceId = "deviceId";
            public static final String type = "type";
            public static final String expires = "expires";
            public static final String expiresFormatted = "expiresFormatted";
            public static final String status = "status";
            public static final String manufacturerId = "manufacturerId";
        }

        public final class TalkerInformation
        {
            public static final String objectName = "talkerInformation";
            public static final String alias = "alias";
            public static final String nodeId = "nodeId";
            public static final String rxFlags = "rxFlags";
            public static final String txPriority = "txPriority";
            public static final String txId = "txId";
            public static final String aliasSpecializer = "aliasSpecializer";
            public static final String rxMuted = "rxMuted";
        }

        public final class GroupTalkers
        {
            public static final String objectName = "groupTalkers";
            public static final String list = "list";
        }

        public final class RtpProfile
        {
            public static final String objectName = "rtpProfile";
            public static final String mode = "mode";
            public static final String jitterMaxMs = "jitterMaxMs";
            public static final String jitterMinMs = "jitterMinMs";
            public static final String jitterMaxFactor = "jitterMaxFactor";
            public static final String latePacketSequenceRange = "latePacketSequenceRange";
            public static final String latePacketTimestampRangeMs = "latePacketTimestampRangeMs";
            public static final String jitterTrimPercentage = "jitterTrimPercentage";
            public static final String jitterUnderrunReductionThresholdMs = "jitterUnderrunReductionThresholdMs";
            public static final String jitterUnderrunReductionAger = "jitterUnderrunReductionAger";
            public static final String jitterForceTrimAtMs = "jitterForceTrimAtMs";
            public static final String jitterMaxTrimMs = "jitterMaxTrimMs";
            public static final String jitterMaxExceededClipPerc = "jitterMaxExceededClipPerc";
            public static final String jitterMaxExceededClipHangMs = "jitterMaxExceededClipHangMs";
            public static final String inboundProcessorInactivityMs = "inboundProcessorInactivityMs";
            public static final String rtcpPresenceTimeoutMs = "rtcpPresenceTimeoutMs";
            public static final String zombieLifetimeMs = "zombieLifetimeMs";
            public static final String signalledInboundProcessorInactivityMs = "signalledInboundProcessorInactivityMs";            
        }

        public final class EnginePolicy
        {
            public final class Database
            {
                public static final String objectName = "database";
                public static final String enabled = "enabled";
                public static final String type = "type";
                public static final String fixedFileName = "fixedFileName";
            }

            public final class Internals
            {
                public static final String objectName = "internals";
                public static final String housekeeperIntervalMs = "housekeeperIntervalMs";
                public static final String logTaskQueueStatsIntervalMs = "logTaskQueueStatsIntervalMs";
                public static final String maxTxSecs = "maxTxSecs";
                public static final String maxRxSecs = "maxRxSecs";
                public static final String enableLazySpeakerClosure = "enableLazySpeakerClosure";
                public static final String rtpExpirationCheckIntervalMs = "rtpExpirationCheckIntervalMs";
                public static final String delayedMicrophoneClosureSecs = "delayedMicrophoneClosureSecs";                
            }

            public final class Timelines
            {
                public static final String objectName = "timelines";
                public static final String enabled = "enabled";
                public static final String maxEventAgeSecs = "maxEventAgeSecs";
	            public static final String storageRoot = "storageRoot";
                public static final String maxStorageMb = "maxStorageMb";
                public static final String maxMemMb = "maxMemMb";
                public static final String maxAudioEventMemMb = "maxAudioEventMemMb";                
                public static final String maxDiskMb = "maxDiskMb";
	            public static final String maxEvents = "maxEvents";
                public static final String groomingIntervalSecs = "groomingIntervalSecs";
                public static final String autosaveIntervalSecs = "autosaveIntervalSecs";
                public static final String disableSigningAndVerification = "disableSigningAndVerification";
                public static final String ephemeral = "ephemeral";                
            }

            public final class Security
            {
                public static final String objectName = "security";

                public final class Certificate
                {
                    public static final String objectName = "certificate";
                    public static final String certificate = "certificate";
                    public static final String key = "key";
                }
            }

            public final class Licensing
            {
                public static final String objectName = "licensing";
                public static final String entitlement = "entitlement";
                public static final String key = "key";
                public static final String activationCode = "activationCode";
                public static final String manufacturerId = "manufacturerId";
            }

            public final class Networking
            {
                public static final String objectName = "networking";
                public static final String defaultNic = "defaultNic";
                public static final String maxOutputQueuePackets = "maxOutputQueuePackets";
                public static final String multicastRejoinSecs = "multicastRejoinSecs";
                public static final String rpLeafConnectTimeoutSecs = "rpLeafConnectTimeoutSecs";
                public static final String maxReconnectPauseMs = "maxReconnectPauseMs";
                public static final String reconnectFailurePauseIncrementMs = "reconnectFailurePauseIncrementMs";
                public static final String sendFailurePauseMs = "sendFailurePauseMs";
                public static final String rallypointRtTestIntervalMs = "rallypointRtTestIntervalMs";
                public static final String logRtpJitterBufferStats = "logRtpJitterBufferStats";
                public static final String preventMulticastFailover = "preventMulticastFailover";
                public static final String rtpProfile = "rtpProfile";
            }

            public final class Audio
            {
                public static final String objectName = "audio";
                public static final String enabled = "enabled";
                public static final String internalRate = "internalRate";
                public static final String internalChannels = "internalChannels";
                public static final String allowOutputOnTransmit = "allowOutputOnTransmit";
                public static final String muteTxOnTx = "muteTxOnTx";
                public static final String denoiseInput = "denoiseInput";
                public static final String denoiseOutput = "denoiseOutput";
                public static final String saveInputPcm = "saveInputPcm";
                public static final String saveOutputPcm = "saveOutputPcm";
                
                public final class Aec
                {
                    public static final String objectName = "aec";
                    public static final String enabled = "enabled";
                    public static final String mode = "mode";
                    public static final String speakerTailMs = "speakerTailMs";
                    public static final String cng = "cng";
                }

                public final class Vad
                {
                    public static final String objectName = "vad";
                    public static final String enabled = "enabled";
                    public static final String mode = "mode";
                }

                public final class Android
                {
                    public static final String objectName = "android";
                    public static final String api = "api";
                    public static final String sharingMode = "sharingMode";
                    public static final String performanceMode = "performanceMode";
                    public static final String usage = "usage";
                    public static final String contentType = "contentType";
                    public static final String inputPreset = "inputPreset";
                    public static final String sessionId = "sessionId";
                    public static final String engineMode = "engineMode";
                }

                public final class InputAgc
                {
                    public static final String objectName = "inputAgc";
                    public static final String enabled = "enabled";
                    public static final String minLevel = "minLevel";
                    public static final String maxLevel = "maxLevel";
                    public static final String compressionGainDb = "compressionGainDb";
                    public static final String enableLimiter = "enableLimiter";
                    public static final String targetLevelDb = "targetLevelDb";
                }

                public final class OutputAgc
                {
                    public static final String objectName = "outputAgc";
                    public static final String enabled = "enabled";
                    public static final String minLevel = "minLevel";
                    public static final String maxLevel = "maxLevel";
                    public static final String compressionGainDb = "compressionGainDb";
                    public static final String enableLimiter = "enableLimiter";
                    public static final String targetLevelDb = "targetLevelDb";
                }
            }

            public final class Discovery
            {
                public static final String objectName = "discovery";

                public final class Magellan
                {
                    public static final String objectName = "magellan";
                    public static final String enabled = "enabled";
                }

                public final class Ssdp
                {
                    public static final String objectName = "ssdp";
                    public static final String enabled = "enabled";
                    public static final String ageTimeoutMs = "ageTimeoutMs";
                    public static final String address = "address";
                }

                public final class Cistech
                {
                    public static final String objectName = "cistech";
                    public static final String enabled = "enabled";
                    public static final String ageTimeoutMs = "ageTimeoutMs";
                    public static final String address = "address";
                }

                public final class Trellisware
                {
                    public static final String objectName = "trellisware";
                    public static final String enabled = "enabled";
                }
            }

            public static final String dataDirectory = "dataDirectory";
        }

        public final class Mission
        {
            public static final String id = "id";
            public static final String name = "name";
            public static final String description = "description";
            public static final String modPin = "modPin";
            public static final String certStoreId = "certStoreId";
            public static final String multicastFailoverPolicy = "multicastFailoverPolicy";
        }

        public final class Rallypoint
        {
            public static final String objectName = "rallypoint";
            public static final String arrayName = "rallypoints";

            public final class Host
            {
                public static final String objectName = "host";
                public static final String address = "address";
                public static final String port = "port";
            }

            public static final String protocol = "protocol";
            public static final String certificate = "certificate";
            public static final String certificateKey = "certificateKey";
            public static final String verifyPeer = "verifyPeer";
            public static final String allowSelfSignedCertificate = "allowSelfSignedCertificate";
            public static final String transactionTimeoutMs = "transactionTimeoutMs";
            public static final String connectionTimeoutSecs = "connectionTimeoutSecs";            
            public static final String disableMessageSigning = "disableMessageSigning";
            public static final String use = "use";
        }

        public final class Address
        {
            public static final String objectName = "address";
            public static final String address = "address";
            public static final String port = "port";
        }

        public final class Rx
        {
            public static final String objectName = "rx";
            public static final String address = "address";
            public static final String port = "port";
        }

        public final class Tx
        {
            public static final String objectName = "tx";
            public static final String address = "address";
            public static final String port = "port";
        }

        public final class RangerPackets
        {
            public static final String objectName = "rangerPackets";
            public static final String hangTimerSecs = "hangTimerSecs";
            public static final String count = "count";
        }

        public final class Group
        {
            public static final String objectName = "group";
            public static final String arrayName = "groups";
            public static final String id = "id";
            public static final String name = "name";
            public static final String spokenName = "spokenName";
            public static final String type = "type";
            public static final String source = "source";
            public static final String cryptoPassword = "cryptoPassword";
            public static final String alias = "alias";
            public static final String maxRxSecs = "maxRxSecs";
            public static final String enableMulticastFailover = "enableMulticastFailover";
            public static final String multicastFailoverSecs = "multicastFailoverSecs";
            public static final String interfaceName = "interfaceName";
            public static final String anonymousAlias = "anonymousAlias";
            public static final String lbCrypto = "lbCrypto";
            public static final String rtpProfile = "rtpProfile";
            public static final String specializerAffinities = "specializerAffinities";
            public static final String languageCode = "languageCode";

            public final class Timeline
            {
                public static final String objectName = "timeline";
                public static final String enabled = "enabled";
            }

            public final class Audio
            {
                public static final String objectName = "audio";
                public static final String inputId = "inputId";
                public static final String outputId = "outputId";
            }

            public final class PriorityTranslation
            {
                public static final String objectName = "priorityTranslation";
                public static final String priority = "priority";
                public static final String rx = "rx";
                public static final String tx = "tx";
            }
        }

        public final class TxAudio
        {
            public static final String objectName = "txAudio";
            public static final String fdx = "fdx";
            public static final String encoder = "encoder";
            public static final String framingMs = "framingMs";
            public static final String maxTxSecs = "maxTxSecs";
            public static final String noHdrExt = "noHdrExt";
            public static final String customRtpPayloadType = "customRtpPayloadType";
            public static final String encoderName = "encoderName";
            public static final String extensionSendInterval = "extensionSendInterval";
            public static final String initialHeaderBurst = "initialHeaderBurst";
            public static final String trailingHeaderBurst = "initialHeaderBurst";
            public static final String enableSmoothing = "enableSmoothing";
            public static final String dtx = "dtx";
            public static final String smoothedHangTimeMs = "smoothedHangTimeMs";
            public static final String resetRtpOnTx = "resetRtpOnTx";
            public static final String startTxNotifications = "startTxNotifications";
        }

        public final class NetworkTxOptions
        {
            public static final String objectName = "txOptions";
            public static final String priority = "priority";
            public static final String ttl = "ttl";
        }

        public final class Presence
        {
            public static final String objectName = "presence";
            public static final String format = "format";
            public static final String intervalSecs = "intervalSecs";
            public static final String listenOnly = "listenOnly";
            public static final String minIntervalSecs = "minIntervalSecs";
        }

        public final class PresenceDescriptor
        {
            public static final String objectName = "presence";
            public static final String self = "self";
            public static final String comment = "comment";
            public static final String custom = "custom";

            public class GroupItem
            {
                public static final String arrayName = "groupAliases";
                public static final String id = "groupId";
                public static final String alias = "alias";
                public static final String status = "status";
            }
        }

        public final class Identity
        {
            public static final String objectName = "identity";
            public static final String nodeId = "nodeId";
            public static final String userId = "userId";
            public static final String displayName = "displayName";
            public static final String type = "type";
            public static final String format = "format";
            public static final String avatar = "avatar";
        }

        public final class Location
        {
            public static final String objectName = "location";
            public static final String longitude = "longitude";
            public static final String latitude = "latitude";
            public static final String altitude = "altitude";
            public static final String direction = "direction";
            public static final String speed = "speed";
        }

        public final class Connectivity
        {
            public static final String objectName = "connectivity";
            public static final String type = "type";
            public static final String strength = "strength";
            public static final String rating = "rating";
        }

        public final class Power
        {
            public static final String objectName = "power";
            public static final String source = "source";
            public static final String state = "state";
            public static final String level = "level";
        }

        public final class RtpHeader
        {
            public static final String objectName = "rtpHeader";
            public static final String pt = "pt";
            public static final String marker = "marker";
            public static final String seq = "seq";
            public static final String ssrc = "ssrc";
            public static final String ts = "ts";
        }

        public final class BlobInfo
        {
            public static final String objectName = "blobInfo";
            public static final String source = "source";
            public static final String target = "target";
            public static final String payloadType = "payloadType";
            public static final String blobSize = "size";
            public static final String rtpHeader = "rtpHeader";
        }

        public final class RiffDescriptor
        {
            public static final String objectName = "descriptor";
            public static final String file = "file";
            public static final String verified = "verified";
            public static final String channels = "channels";
            public static final String sampleCount = "sampleCount";
            public static final String meta = "meta";
            public static final String certificate = "certificate";
            public static final String signature = "signature";
        }

        public final class TimelineEvent
        {
            public final class Audio
            {
                public static final String objectName = "audio";
                public static final String ms = "ms";
                public static final String samples = "samples";
            }

            public static final String objectName = "event";
            public static final String alias = "alias";
            public static final String direction = "direction";
            public static final String ended = "ended";
            public static final String groupId = "groupId";
            public static final String id = "id";
            public static final String inProgress = "inProgress";
            public static final String nodeId = "nodeId";
            public static final String started = "started";
            public static final String thisNodeId = "thisNodeId";
            public static final String type = "type";
            public static final String uri = "uri";
            public static final String jsonAttachment = "jsonAttachment";
            public static final String blobAttachment = "blobAttachment";
        }

        public final class TimelineQuery
        {
            public static final String maxCount = "maxCount";
            public static final String mostRecentFirst = "mostRecentFirst";
            public static final String startedOnOrAfter = "startedOnOrAfter";
            public static final String endedOnOrBefore = "endedOnOrBefore";
            public static final String onlyDirection = "onlyDirection";
            public static final String onlyType = "onlyType";
            public static final String onlyCommitted = "onlyCommitted";
            public static final String onlyAlias = "onlyAlias";
            public static final String onlyNodeId = "onlyNodeId";
            public static final String onlyTxId = "onlyTxId";
            public static final String sql = "sql";
        }

        public final class TimelineReport
        {
            public static final String success = "success";
            public static final String errorMessage = "errorMessage";
            public static final String started = "started";
            public static final String ended = "ended";
            public static final String execMs = "execMs";
            public static final String records = "records";
            public static final String events = "events";
            public static final String count = "count";
        }
    }

    private static final String TAG = Engine.class.getSimpleName();

    public static String bytesToHexString(byte[] bytes)
    {
        if(bytes == null || bytes.length == 0)
        {
            return null;
        }

        StringBuffer hs = new StringBuffer();
        for (int x = 0; x < bytes.length; x++)
        {
            String s = Integer.toHexString(0xFF & bytes[x]);
            if(s.length() == 1)
            {
                s = "0" + s;
            }

            hs.append(s);
        }

        return hs.toString().toUpperCase();
    }

    private Handler _handler = null;
    private static Context _appContext = null;
    private int _audioSessionId = 0;
    private static AudioManager _audioManager = null;
    private static IAudioProvider _audioProvider = null;

    // IMPORTANT:  Call this as soon as the application context has been created!
    public static void setApplicationContext(Context appContext)
    {
        _appContext = appContext;
    }

    public static void setApplicationAudioManager(AudioManager appAm)
    {
        _audioManager = appAm;
    }

    public static void setApplicationAudioProvider(IAudioProvider audioProvider)
    {
        _audioProvider = audioProvider;
    }

    public static IAudioProvider getApplicationAudioProvider()
    {
        return _audioProvider;
    }

    public void initialize()
    {
        _handler = new Handler();
    }

    public void deinitialize()
    {
    }

    public EngageDatagram allocateDatagram(byte[] data, int status)
    {
        EngageDatagram rc = new EngageDatagram();

        rc.setBytes(data);
        rc.setStatus(status);

        return rc;
    }

    public EngageDatagram allocateDatagram(byte[] data)
    {
        EngageDatagram rc = allocateDatagram(data, data.length);

        return rc;
    }

    public EngageDatagram allocateDatagram()
    {
        return allocateDatagram(null, 0);
    }

    public interface IEngineListener
    {
        void onEngineStarted(String eventExtraJson);
        void onEngineStartFailed(String eventExtraJson);
        void onEngineStopped(String eventExtraJson);
        void onEngineAudioDevicesRefreshed(String eventExtraJson);
        void onEngineGroupByGroupPcmPowerLevels(String eventExtraJson);
        void onEngineAudioDeviceEvent(String eventExtraJson);
    }

    
    public interface IRallypointListener
    {
        void onRallypointPausingConnectionAttempt(String id, String eventExtraJson);
        void onRallypointConnecting(String id, String eventExtraJson);
        void onRallypointConnected(String id, String eventExtraJson);
        void onRallypointDisconnected(String id, String eventExtraJson);
        void onRallypointRoundtripReport(String id, long rtMs, long rtQualityRating, String eventExtraJson);
    }

    
    public interface IGroupListener
    {
        void onGroupCreated(String id, String eventExtraJson);
        void onGroupCreateFailed(String id, String eventExtraJson);
        void onGroupDeleted(String id, String eventExtraJson);
        void onGroupConnected(String id, String eventExtraJson);
        void onGroupConnectFailed(String id, String eventExtraJson);
        void onGroupDisconnected(String id, String eventExtraJson);
        void onGroupJoined(String id, String eventExtraJson);
        void onGroupJoinFailed(String id, String eventExtraJson);
        void onGroupLeft(String id, String eventExtraJson);
        void onGroupMemberCountChanged(String id, long count, String eventExtraJson);
        void onGroupRxStarted(String id, String eventExtraJson);
        void onGroupRxEnded(String id, String eventExtraJson);
        void onGroupRxSpeakersChanged(String id, String groupTalkerJson, String eventExtraJson);
        void onGroupRxMuted(String id, String eventExtraJson);
        void onGroupRxUnmuted(String id, String eventExtraJson);
        void onGroupTxMuted(String id, String eventExtraJson);
        void onGroupTxUnmuted(String id, String eventExtraJson);
        void onGroupTxStarted(String id, String eventExtraJson);
        void onGroupTxEnded(String id, String eventExtraJson);
        void onGroupTxFailed(String id, String eventExtraJson);
        void onGroupTxUsurpedByPriority(String id, String eventExtraJson);
        void onGroupMaxTxTimeExceeded(String id, String eventExtraJson);
        void onGroupNodeDiscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupNodeRediscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupNodeUndiscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupAssetDiscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupAssetRediscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupAssetUndiscovered(String id, String nodeJson, String eventExtraJson);
        void onGroupBlobSent(String id, String eventExtraJson);
        void onGroupBlobSendFailed(String id, String eventExtraJson);
        void onGroupBlobReceived(String id, String blobInfoJson, byte[] blob, long blobSize, String eventExtraJson);
        void onGroupRtpSent(String id, String eventExtraJson);
        void onGroupRtpSendFailed(String id, String eventExtraJson);
        void onGroupRtpReceived(String id, String blobInfoJson, byte[] payload, long payloadSize, String eventExtraJson);
        void onGroupRawSent(String id, String eventExtraJson);
        void onGroupRawSendFailed(String id, String eventExtraJson);
        void onGroupRawReceived(String id, byte[] raw, long rawSize, String eventExtraJson);

        void onGroupTimelineEventStarted(String id, String eventJson, String eventExtraJson);
        void onGroupTimelineEventUpdated(String id, String eventJson, String eventExtraJson);
        void onGroupTimelineEventEnded(String id, String eventJson, String eventExtraJson);
        void onGroupTimelineReport(String id, String reportJson, String eventExtraJson);
        void onGroupTimelineReportFailed(String id, String eventExtraJson);
        void onGroupTimelineGroomed(String id, String eventJson, String eventExtraJson);       

        void onGroupHealthReport(String id, String healthReportJson, String eventExtraJson);
        void onGroupHealthReportFailed(String id, String eventExtraJson);

        void onGroupStatsReport(String id, String statsReportJson, String eventExtraJson);
        void onGroupStatsReportFailed(String id, String eventExtraJson);

        void onGroupRxVolumeChanged(String id, int leftLevelPerc, int rightLevelPerc, String eventExtraJson);
        void onGroupRxDtmf(String id, String dtmfJson, String eventExtraJson);

        void onGroupReconfigured(String id, String eventExtraJson);
        void onGroupReconfigurationFailed(String id, String eventExtraJson);
    }

    public interface IBridgeListener
    {
        void onBridgeCreated(String id, String eventExtraJson);
        void onBridgeCreateFailed(String id, String eventExtraJson);
        void onBridgeDeleted(String id, String eventExtraJson);
    }
    
    public interface ILicenseListener
    {
        void onLicenseChanged(String eventExtraJson);
        void onLicenseExpired(String eventExtraJson);
        void onLicenseExpiring(double secondsLeft, String eventExtraJson);
    }

    
    public interface ILoggingListener
    {
        void onEngageLogMessage(LoggingLevel level, String tag, String message);
    }

    
    public interface IAppNetworkDeviceListener
    {
        int onAppNetworkDeviceStart(long deviceId, String jsonMetaData);
        int onAppNetworkDeviceStop(long deviceId, String jsonMetaData);
        EngageDatagram onAppNetworkDeviceRecvEngageDatagram(int deviceId, int timeoutMs, String jsonMetaData);
        int onAppNetworkDeviceSendEngageDatagram(int deviceId, EngageDatagram dg, int timeoutMs, String jsonMetaData);
    }

    public interface IAppAudioSpecializationListener
    {
        int onAppAudioSpecialization(int p1, int p2, int p3, int p4);
    }

    // Listeners
    private ArrayList<IEngineListener> _engineListeners = new ArrayList<>();
    private ArrayList<IRallypointListener> _rallypointListeners = new ArrayList<>();
    private ArrayList<IGroupListener> _groupListeners = new ArrayList<>();
    private ArrayList<ILicenseListener> _licenseListeners = new ArrayList<>();
    private ArrayList<ILoggingListener> _loggingListeners = new ArrayList<>();
    private IAppNetworkDeviceListener _appNetworkDeviceListener = null;
    private IAppAudioSpecializationListener _appAudioSpecializationListener = null;
    private ArrayList<IBridgeListener> _bridgeListeners = new ArrayList<>();
    
    // Internals
    private MdnsDiscoverer _mdsnDiscoverer = null;

    private class MdnsDiscoverer
    {
        private class DiscoveredEntity
        {
            String _id;
            String _type;
            String _name;

            public JSONObject makeJsonObject(String address, int port)
            {
                JSONObject root = new JSONObject();
                JSONObject addr = new JSONObject();

                try
                {
                    root.put("id", _id);
                    root.put("type", _type);
                    root.put("name", _name);

                    addr.put("address", address);
                    addr.put("port", port);
                    root.put("address", addr);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                    root = null;
                }

                return root;
            }
        }

        private NsdManager _nsdManager = null;
        private NsdManager.ResolveListener _resolveListener;

        private HashMap<String, NsdManager.DiscoveryListener> _registeredTypes = new HashMap<>();
        private ArrayList<DiscoveredEntity> _discoveredEntities = new ArrayList<>();


        public void addListener(final String serviceType)
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    String ast = unadornedServiceType(serviceType);

                    if( _registeredTypes.containsKey(ast))
                    {
                        return;
                    }

                    if (_nsdManager == null)
                    {

                        _nsdManager = (NsdManager) _appContext.getSystemService(Context.NSD_SERVICE);
                        initializeResolveListener();
                    }

                    NsdManager.DiscoveryListener dl = createDiscoveryListener();
                    _registeredTypes.put(ast, dl);

                    _nsdManager.discoverServices(ast, NsdManager.PROTOCOL_DNS_SD, dl);
                }
            });
        }


        public void removeListener(final String serviceType)
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    String ast = unadornedServiceType(serviceType);

                    if(_registeredTypes.containsKey(unadornedServiceType(ast)))
                    {
                        NsdManager.DiscoveryListener dl = _registeredTypes.get(unadornedServiceType(ast));
                        _registeredTypes.remove(ast);
                        _nsdManager.stopServiceDiscovery(dl);
                    }

                    clearAllDiscoveredEntitiesForType(ast);
                }
            });
        }

        private String unadornedServiceType(String serviceType)
        {
            String rc = serviceType;


            if(rc.endsWith("."))
            {
                rc = rc.substring(0, rc.length() - 1);
            }

            if(rc.startsWith("."))
            {
                rc = rc.substring(1);
            }

            return rc;
        }

        private String inetAddressAsString(InetAddress addr)
        {
            String rc = addr.toString();
            if(rc.startsWith("/"))
            {
                rc = rc.substring(1);
            }

            return rc;
        }

        private void clearAllDiscoveredEntities()
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    for(DiscoveredEntity de : _discoveredEntities)
                    {
                        engagePlatformServiceUndiscovered(de._id);
                    }

                    _discoveredEntities.clear();
                }
            });
        }

        private void clearAllDiscoveredEntitiesForType(final String serviceType)
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    String ast = unadornedServiceType(serviceType);

                    ArrayList<DiscoveredEntity> trash = new ArrayList<>();

                    for(DiscoveredEntity de : _discoveredEntities)
                    {
                        if(de._type.compareTo(ast) == 0)
                        {
                            trash.add(de);
                        }
                    }

                    for(DiscoveredEntity de : trash)
                    {
                        engagePlatformServiceUndiscovered(de._id);
                        _discoveredEntities.remove(de);
                    }

                    trash.clear();
                }
            });
        }

        private void clearDiscoveredEntity(final String serviceType, final String serviceName)
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    String ast = unadornedServiceType(serviceType);

                    ArrayList<DiscoveredEntity> trash = new ArrayList<>();

                    for(DiscoveredEntity de : _discoveredEntities)
                    {
                        if(de._type.compareTo(ast) == 0 && de._name.compareTo(serviceName) == 0)
                        {
                            trash.add(de);
                        }
                    }

                    for(DiscoveredEntity de : trash)
                    {
                        engagePlatformServiceUndiscovered(de._id);
                        _discoveredEntities.remove(de);
                    }

                    trash.clear();
                }
            });
        }

        private void cleanup()
        {
            _handler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    for(NsdManager.DiscoveryListener dl : _registeredTypes.values())
                    {
                        _nsdManager.stopServiceDiscovery(dl);
                    }

                    _registeredTypes.clear();

                    clearAllDiscoveredEntities();

                    _nsdManager = null;
                }
            });
        }

        private NsdManager.DiscoveryListener getListenerForServiceType(String serviceType)
        {
            String ast = unadornedServiceType(serviceType);

            if(!_registeredTypes.containsKey(ast))
            {
                return null;
            }

            return _registeredTypes.get(ast);
        }

        private DiscoveredEntity getDiscoveredEntity(String serviceType, String serviceName)
        {
            String ast = unadornedServiceType(serviceType);

            for(DiscoveredEntity de : _discoveredEntities)
            {
                if(de._type.compareTo(ast) == 0 && de._name.compareTo(serviceName) == 0)
                {
                    return de;
                }
            }

            return null;
        }

        private NsdManager.DiscoveryListener createDiscoveryListener()
        {
            NsdManager.DiscoveryListener rc = new NsdManager.DiscoveryListener()
            {
                // Called as soon as service discovery begins.
                @Override
                public void onDiscoveryStarted(final String regType)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_D(TAG, ">>>>==== Service discovery started");
                        }
                    });

                }

                @Override
                public void onServiceFound(final NsdServiceInfo service)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_D(TAG, ">>>>==== Service discovery success " + service);

                            String ast = unadornedServiceType(service.getServiceType());

                            NsdManager.DiscoveryListener dl = getListenerForServiceType(ast);
                            if(dl != null)
                            {
                                _nsdManager.resolveService(service, _resolveListener);
                            }
                        }
                    });
                }

                @Override
                public void onServiceLost(final NsdServiceInfo service)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_E(TAG, ">>>>==== Service lost: " + service);

                            clearDiscoveredEntity(service.getServiceType(), service.getServiceName());
                        }
                    });
                }

                @Override
                public void onDiscoveryStopped(final String serviceType)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_I(TAG, ">>>>==== Discovery stopped: " + serviceType);

                            clearAllDiscoveredEntitiesForType(serviceType);
                        }
                    });
                }

                @Override
                public void onStartDiscoveryFailed(final String serviceType, final int errorCode)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_E(TAG, ">>>>==== Discovery failed: Error code:" + errorCode);

                            clearAllDiscoveredEntitiesForType(serviceType);

                            NsdManager.DiscoveryListener dl = getListenerForServiceType(unadornedServiceType(serviceType));
                            if(dl != null)
                            {
                                _nsdManager.stopServiceDiscovery(dl);
                            }
                        }
                    });
                }

                @Override
                public void onStopDiscoveryFailed(final String serviceType, final int errorCode)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_E(TAG, ">>>>==== Discovery failed: Error code:" + errorCode);
                        }
                    });
                }
            };


            return rc;
        }

        private void initializeResolveListener()
        {
            _resolveListener = new NsdManager.ResolveListener()
            {
                @Override
                public void onResolveFailed(final NsdServiceInfo serviceInfo, final int errorCode)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_E(TAG, ">>>>==== Resolve failed: " + errorCode);
                        }
                    });
                }

                @Override
                public void onServiceResolved(final NsdServiceInfo serviceInfo)
                {
                    _handler.post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            //shimLayerInternalLogging_E(TAG, ">>>>==== Resolve Succeeded. " + serviceInfo);

                            String ast = unadornedServiceType(serviceInfo.getServiceType());
                            int port = serviceInfo.getPort();
                            String address = inetAddressAsString(serviceInfo.getHost());

                            DiscoveredEntity de = getDiscoveredEntity(ast, serviceInfo.getServiceName());
                            if(de == null)
                            {
                                de = new DiscoveredEntity();
                                de._id = "{" + UUID.randomUUID().toString() + "}";
                                de._type = ast;
                                de._name = serviceInfo.getServiceName();
                                _discoveredEntities.add(de);

                                JSONObject obj = de.makeJsonObject(address, port);
                                if(obj != null)
                                {
                                    engagePlatformServiceDiscovered(de._id, obj.toString());
                                }
                            }
                            else
                            {
                                JSONObject obj = de.makeJsonObject(address, port);
                                if(obj != null)
                                {
                                    engagePlatformServiceRediscovered(de._id, obj.toString());
                                }
                            }
                        }
                    });
                }
            };
        }
    }

    
    public void addEngineListener(final IEngineListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                {
                    _engineListeners.add(listener);
                }
            }
        });
    }

    
    public void removeEngineListener(final IEngineListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _engineListeners.remove(listener);
            }
        });
    }

    
    public void addRallypointListener(final IRallypointListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _rallypointListeners.add(listener);
            }
        });
    }

    
    public void removeRallypointListener(final IRallypointListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _rallypointListeners.remove(listener);
            }
        });
    }

    
    public void addGroupListener(final IGroupListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _groupListeners.add(listener);
            }
        });
    }

    
    public void removeGroupListener(final IGroupListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _groupListeners.remove(listener);
            }
        });
    }

    public void addBridgeListener(final IBridgeListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _bridgeListeners.add(listener);
            }
        });
    }

    
    public void removeBridgeListener(final IBridgeListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _bridgeListeners.remove(listener);
            }
        });
    }
    
    public void addLicenseListener(final ILicenseListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _licenseListeners.add(listener);
            }
        });
    }

    
    public void removeLicenseListener(final ILicenseListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _licenseListeners.remove(listener);
            }
        });
    }

    
    public void addLoggingListener(final ILoggingListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _loggingListeners.add(listener);

                if(_loggingListeners.size() == 1)
                {
                    engageSetLoggingOutputOverride("engineLoggingHook");
                }
            }
        });
    }

    
    public void removeLoggingListener(final ILoggingListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _loggingListeners.remove(listener);

                if(_loggingListeners.size() == 0)
                {
                    engageSetLoggingOutputOverride("");
                }
            }
        });
    }    


    public void setAppAudioSpecializationListener(final IAppAudioSpecializationListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _appAudioSpecializationListener = listener;
            }
        });
    }
    
    public void setAppNetworkDeviceListener(final IAppNetworkDeviceListener listener)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                _appNetworkDeviceListener = listener;
            }
        });
    }
    
    // API calls
    
    public native String engageGetVersion();

    
    public native String engageGetHardwareReport();    

    
    public native String engageGetNetworkInterfaceDevices();

    
    public native String engageGetAudioDevices();

    
    public native int engageInitialize(String enginePolicyConfiguration, String userIdentity, String tempStoragePath);

    
    public native int engageShutdown();

    
    public native int engageStart();

    
    public native int engageStop();

    
    public native int engageCreateGroup(String jsonConfiguration);

    
    public native int engageReconfigureGroup(String id, String jsonConfiguration);

    
    public native int engageDeleteGroup(String id);

    
    public native int engageJoinGroup(String id);

    
    public native int engageLeaveGroup(String id);


    public native int engageSetGroupRules(String id, String jsonParams);
    
    
    public native int engageBeginGroupTx(String id, int txPriority, int txFlags);

    
    public native int engageBeginGroupTxAdvanced(String id, String jsonParams);

    
    public native int engageEndGroupTx(String id);

    
    public native int engageSetGroupRxTag(String id, int tag);

    
    public native int engageMuteGroupRx(String id);

    
    public native int engageUnmuteGroupRx(String id);

    
    public native int engageMuteGroupTx(String id);

    
    public native int engageUnmuteGroupTx(String id);

    
    public native int engageSetGroupRxVolume(String id, int left, int right);

    
    public native int engageUpdatePresenceDescriptor(String id, String jsonDescriptor, int forceBeacon);

    
    public native int engageSendGroupBlob(String id, byte[] blob, int size, String jsonParams);

    
    public native int engageSendGroupRtp(String id, byte[] payload, int size, String jsonParams);

    
    public native int engageSendGroupRaw(String id, byte[] raw, int size, String jsonParams);

    
    public native int engageRegisterGroupRtpHandler(String id, int payloadId);

    
    public native int engageUnregisterGroupRtpHandler(String id, int payloadId);

    
    public native int engageEncrypt(byte[] src, int size, byte[] dst, String passwordHexByteString);

    
    public byte[] encryptSimple(byte[] clearData, String passwordHexByteString)
    {
        byte[] rc = null;

        try
        {
            byte[] dst = new byte[clearData.length + 16 + 512];
            int transformedLen = engageEncrypt(clearData, clearData.length, dst, passwordHexByteString);
            if(transformedLen > 0)
            {
                rc = new byte[transformedLen];
                System.arraycopy(dst, 0, rc, 0, transformedLen);
            }
            System.gc();
        }
        catch (Exception e)
        {
            rc = null;
            e.printStackTrace();
        }

        return rc;
    }

    
    public byte[] decryptSimple(byte[] clearData, String passwordHexByteString)
    {
        byte[] rc = null;

        try
        {
            byte[] dst = new byte[clearData.length + 16 + 512];
            int transformedLen = engageDecrypt(clearData, clearData.length, dst, passwordHexByteString);
            if(transformedLen > 0)
            {
                rc = new byte[transformedLen];
                System.arraycopy(dst, 0, rc, 0, transformedLen);
            }
            System.gc();
        }
        catch (Exception e)
        {
            rc = null;
            e.printStackTrace();
        }

        return rc;
    }
    
    public native int engageDecrypt(byte[] src,
                                    int size,
                                    byte[] dst,
                                    String passwordHexByteString);

    
    public native String engageGetActiveLicenseDescriptor();

    
    public native String engageGetLicenseDescriptor(String entitlement, String key, String activationCode, String manufacturerId);

    
    public native int engageUpdateLicense(String entitlement, String key, String activationCode, String manufacturerId);

    
    public native int engagePlatformServiceDiscovered(String id, String jsonParams);

    
    public native int engagePlatformServiceRediscovered(String id, String jsonParams);

    
    public native int engagePlatformServiceUndiscovered(String id);

    
    public native int engageQueryGroupTimeline(String id, String jsonParams);

    
    public native int engageQueryGroupHealth(String id);

    
    public native int engageQueryGroupStats(String id);

    
    public native int engageSetLogLevel(int level);

    
    public native int engageSetLogTagExtension(String tagExtension);    

    
    public native int engageEnableSyslog(int enable);

    
    public native int engageEnableWatchdog(int enable);

    
    public native int engageLogMsg(int level, String tag, String msg);

    
    public native String engageGenerateMission(String keyPhrase, int audioGroupCount, String rallypointHost, String missionName);

    
    public native String engageGenerateMissionUsingCertStore(String keyPhrase, int audioGroupCount, String rallypointHost, String missionName, String certStoreFn, String certStorePasswordHexByteString, String certStoreElement);

    
    public native int engageSetMissionId(String missionId);

    
    public native int engageOpenCertStore(String fileName, String passwordHexByteString);

    
    public native String engageGetCertStoreDescriptor();

    
    public native int engageCloseCertStore();

    
    public native int engageSetCertStoreCertificatePem(String id, String certificatePem, String privateKeyPem, String tags);

    
    public native int engageSetCertStoreCertificateP12(String id, byte[] data, int size, String password, String tags);

    
    public native int engageDeleteCertStoreCertificate(String id);

    
    public native String engageGetCertStoreCertificatePem(String id);

    
    public native String engageGetCertificateDescriptorFromPem(String pem);

    public native String engageGetArrayOfCertificateDescriptorsFromPem(String pem);
    
    public native int engageImportCertStoreElementFromCertStore(String id, String srcId, String srcFileName, String srcPasswordHexByteString, String tags);

    
    public native String engageQueryCertStoreContents(String fileName, String passwordHexByteString);

    
    public native int engagePlatformNotifyChanges(String jsonChangesArray);

    
    public native int engageNetworkDeviceRegister(String jsonConfiguration);
    
    public native int engageNetworkDeviceUnregister(int deviceId);


    public native int engageBeginGroupPcmPowerTracking(String id);

    public native int engageEndGroupPcmPowerTracking(String id);

    // Logging override hook
    
    private native int engageSetLoggingOutputOverride(String pfnHook);

    @Keep
    private void engineLoggingHook(final int level, final String tag, final String message)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                LoggingLevel enumLoggingLevel = LoggingLevel.fromInt(level);
                for (ILoggingListener listener : _loggingListeners)
                {
                    listener.onEngageLogMessage(enumLoggingLevel, tag, message);
                }                
            }
        });
    }

    private void shimLayerInternalLogging_Generic(final int level, final String tag, final String message)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                if(_loggingListeners.size() > 0)
                {
                    engineLoggingHook(level, tag, message);
                }
                else
                {
                    LoggingLevel enumLoggingLevel = LoggingLevel.fromInt(level);

                    if(enumLoggingLevel == LoggingLevel.fatal)
                    {
                        Log.wtf(tag, message);
                    }
                    else if(enumLoggingLevel == LoggingLevel.error)
                    {
                        Log.e(tag, message);
                    }
                    else if(enumLoggingLevel == LoggingLevel.warning)
                    {
                        Log.w(tag, message);
                    }
                    else if(enumLoggingLevel == LoggingLevel.information)
                    {
                        Log.i(tag, message);
                    }
                    else
                    {
                        Log.d(tag, message);
                    }
                }
            }
        });        
    }

    private void shimLayerInternalLogging_F(final String tag, final String message)
    {
        shimLayerInternalLogging_Generic(0, tag, message);
    }

    private void shimLayerInternalLogging_E(final String tag, final String message)
    {
        shimLayerInternalLogging_Generic(1, tag, message);
    }

    private void shimLayerInternalLogging_W(final String tag, final String message)
    {
        shimLayerInternalLogging_Generic(2, tag, message);
    }

    private void shimLayerInternalLogging_I(final String tag, final String message)
    {
        shimLayerInternalLogging_Generic(3, tag, message);
    }

    private void shimLayerInternalLogging_D(final String tag, final String message)
    {
        shimLayerInternalLogging_Generic(4, tag, message);
    }
    
    public native int engageRefreshAudioDevices();

    public native int engageCreateBridge(String jsonConfiguration);
    
    public native int engageDeleteBridge(String id);


    /*
    public native int engageCompress(byte[] src,
                                     int srcSize,
                                     byte[] dst,
                                     int maxDstSize);

    
    public native int engageDecompress(byte[] src,
                                       int srcSize,
                                       byte[] dst,
                                       int maxDstSize);

    public byte[] compressSimple(byte[] input)
    {
        byte[] rc = null;

        try
        {
            byte[] dst;
            int dstLen;

            dstLen = (input.length + 64);
            while( true )
            {
                dstLen *= 2;
                dst = new byte[dstLen];
                int compressedLen = engageCompress(input, input.length, dst, dstLen);
                if(compressedLen > 0)
                {
                    rc = new byte[compressedLen];
                    System.arraycopy(dst, 0, rc, 0, compressedLen);
                    break;
                }
                else
                {
                    if(compressedLen != EngageResult.toInt(EngageResult.insufficientDestinationSpace));
                    {
                        rc = null;
                        break;
                    }
                }
            }

            System.gc();
        }
        catch (Exception e)
        {
            rc = null;
            e.printStackTrace();
        }

        return rc;
    }

    
    public byte[] decompressSimple(byte[] input)
    {
        byte[] rc = null;

        try
        {
            byte[] dst;
            int dstLen;

            dstLen = ((input.length + 64) * 4);
            while( true )
            {
                dstLen *= 2;
                dst = new byte[dstLen];
                int decompressedLen = engageDecompress(input, input.length, dst, dstLen);
                if(decompressedLen > 0)
                {
                    rc = new byte[decompressedLen];
                    System.arraycopy(dst, 0, rc, 0, decompressedLen);
                    break;
                }
                else
                {
                    if(decompressedLen != EngageResult.toInt(EngageResult.insufficientDestinationSpace));
                    {
                        rc = null;
                        break;
                    }
                }
            }

            System.gc();
        }
        catch (Exception e)
        {
            rc = null;
            e.printStackTrace();
        }

        return rc;
    }
    */

    public native int engageSetFipsCrypto(String jsonParams);

    public native int engageIsCryptoFipsValidated();

    public native String engageGetDeviceId();

    public native int engageSetCertStore(byte[] buff, int size, String passwordHexByteString);

    public native int engageVerifyRiff(String fn);

    public native String engageGetRiffDescriptor(String fn);

    // Platform services requests ("upcalls" from the Engine)
    
    @Keep
    private void onPlatformRequestBeginServiceDiscovery(final String serviceType, final int preferredProtocol)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                if (_mdsnDiscoverer == null)
                {
                    _mdsnDiscoverer = new MdnsDiscoverer();
                }

                _mdsnDiscoverer.addListener(serviceType);
            }
        });
    }

    @Keep
    private void onPlatformRequestEndServiceDiscovery(final String serviceType, final int preferredProtocol)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                if (_mdsnDiscoverer == null)
                {
                    return;
                }

                _mdsnDiscoverer.removeListener(serviceType);
            }
        });
    }

    @Keep
    private String onPlatformGetAudioDeviceList(int forInput)
    {
        String rc = null;

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M)
        {
            try
            {
                AudioManager audioManager = (AudioManager) _appContext.getSystemService(Context.AUDIO_SERVICE);
                if(audioManager != null)
                {
                    JSONObject root = new JSONObject();
                    JSONArray list = new JSONArray();

                    final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);

                    for(AudioDeviceInfo d : devices)
                    {
                        if( ((forInput == 1) && d.isSource()) || ((forInput == 0) && d.isSink()) )
                        {
                            JSONObject di = new JSONObject();

                            di.put(JsonFields.AudioDevice.hardwareId, Integer.toString(d.getId()));
                            di.put(JsonFields.AudioDevice.name, d.getProductName());
                            di.put(JsonFields.AudioDevice.type, Integer.toString(d.getType()));
                            di.put(JsonFields.AudioDevice.manufacturer, "");
                            di.put(JsonFields.AudioDevice.model, "");
                            di.put(JsonFields.AudioDevice.serialNumber, "");
                            di.put(JsonFields.AudioDevice.isDefault, false);
                            di.put(JsonFields.AudioDevice.extra, d.getAddress());

                            list.put(di);
                        }
                    }

                    // Add a default device
                    {
                        JSONObject di = new JSONObject();

                        di.put(JsonFields.AudioDevice.hardwareId, "0");
                        di.put(JsonFields.AudioDevice.name, "default");
                        di.put(JsonFields.AudioDevice.type, "0");
                        di.put(JsonFields.AudioDevice.manufacturer, "");
                        di.put(JsonFields.AudioDevice.model, "");
                        di.put(JsonFields.AudioDevice.serialNumber, "");
                        di.put(JsonFields.AudioDevice.isDefault, true);
                        di.put(JsonFields.AudioDevice.extra, "");

                        list.put(di);
                    }

                    root.put(JsonFields.ListOfAudioDevice.objectName, list);
                    rc = root.toString();
                }
                else
                {
                    throw new Exception("onPlatformGetAudioDeviceList: failed to acquire audio manager");
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
                rc = null;
            }
        }

        if(rc == null)
        {
            try
            {
                JSONObject root = new JSONObject();
                JSONArray list = new JSONArray();

                JSONObject di = new JSONObject();

                di.put(JsonFields.AudioDevice.hardwareId, "0");
                di.put(JsonFields.AudioDevice.name, "default");
                di.put(JsonFields.AudioDevice.type, "0");
                di.put(JsonFields.AudioDevice.manufacturer, "");
                di.put(JsonFields.AudioDevice.model, "");
                di.put(JsonFields.AudioDevice.serialNumber, "");
                di.put(JsonFields.AudioDevice.isDefault, true);
                di.put(JsonFields.AudioDevice.extra, "");

                list.put(di);
                root.put(JsonFields.ListOfAudioDevice.objectName, list);
                rc = root.toString();
            }
            catch (Exception e)
            {
                rc = "{}";
            }
        }

        return rc;
    }

    @Keep
    private String onPlatformGetNetworkInterfaceDeviceList()
    {
        String rc = null;

        try
        {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();

            JSONObject root = new JSONObject();
            JSONArray list = new JSONArray();

            while( interfaces.hasMoreElements() )
            {
                NetworkInterface networkInterface = interfaces.nextElement();

                String nm = networkInterface.getName();
                String dn = networkInterface.getDisplayName();
                String hw = bytesToHexString(networkInterface.getHardwareAddress());

                if(dn.compareTo(nm) == 0)
                {
                    dn = "";
                }

                JSONObject di;

                Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();

                if(inetAddresses.hasMoreElements())
                {
                    while (inetAddresses.hasMoreElements())
                    {
                        InetAddress ina = inetAddresses.nextElement();

                        di = new JSONObject();
                        di.put(JsonFields.NetworkInterfaceDevice.name, nm);
                        di.put(JsonFields.NetworkInterfaceDevice.friendlyName, dn);
                        di.put(JsonFields.NetworkInterfaceDevice.description, "");
                        di.put(JsonFields.NetworkInterfaceDevice.hardwareAddress, hw);
                        di.put(JsonFields.NetworkInterfaceDevice.available, networkInterface.isUp());
                        di.put(JsonFields.NetworkInterfaceDevice.isLoopback, networkInterface.isLoopback());
                        di.put(JsonFields.NetworkInterfaceDevice.supportsMulticast, networkInterface.supportsMulticast());

                        if (ina instanceof Inet4Address || ina instanceof Inet6Address)
                        {
                            String sAddress = ina.toString();
                            if (sAddress != null && sAddress.length() > 1)
                            {
                                sAddress = sAddress.substring(1);
                            }

                            di.put(JsonFields.NetworkInterfaceDevice.address, sAddress);

                            if (ina instanceof Inet4Address)
                            {
                                di.put(JsonFields.NetworkInterfaceDevice.family, NetworkDeviceFamily.ipv4.toInt());
                            }
                            else if (ina instanceof Inet6Address)
                            {
                                di.put(JsonFields.NetworkInterfaceDevice.family, NetworkDeviceFamily.ipv6.toInt());
                            }
                            else
                            {
                                di.put(JsonFields.NetworkInterfaceDevice.family, -1);
                            }
                        }

                        list.put(di);
                    }
                }
                else
                {
                    di = new JSONObject();
                    di.put(JsonFields.NetworkInterfaceDevice.name, nm);
                    di.put(JsonFields.NetworkInterfaceDevice.friendlyName, dn);
                    di.put(JsonFields.NetworkInterfaceDevice.description, "");
                    di.put(JsonFields.NetworkInterfaceDevice.hardwareAddress, hw);
                    di.put(JsonFields.NetworkInterfaceDevice.available, networkInterface.isUp());
                    di.put(JsonFields.NetworkInterfaceDevice.isLoopback, networkInterface.isLoopback());
                    di.put(JsonFields.NetworkInterfaceDevice.supportsMulticast, networkInterface.supportsMulticast());
                    di.put(JsonFields.NetworkInterfaceDevice.family, -1);
                    di.put(JsonFields.NetworkInterfaceDevice.address, "");
                    list.put(di);
                }
            }

            root.put(JsonFields.ListOfNetworkInterfaceDevice.objectName, list);
            rc = root.toString();
        }
        catch(Exception e)
        {
            rc = null;
        }

        if(rc == null)
        {
            rc = "{}";
        }

        return rc;
    }

    // Datagrams
    @Keep
    private EngageDatagram onPlatformAllocateDatagram()
    {
        return new EngageDatagram();
    }

    @Keep
    private int onPlatformAppNetworkDeviceStart(final int deviceId, final String jsonMetaData)
    {
        if(_appNetworkDeviceListener == null)
        {
            return -1;
        }
        else
        {
            return _appNetworkDeviceListener.onAppNetworkDeviceStart(deviceId, jsonMetaData);
        }
    }

    @Keep
    private int onPlatformAppNetworkDeviceStop(final int deviceId, final String jsonMetaData)
    {
        if(_appNetworkDeviceListener == null)
        {
            return -1;
        }
        else
        {
            return _appNetworkDeviceListener.onAppNetworkDeviceStop(deviceId, jsonMetaData);
        }
    }

    @Keep
    private EngageDatagram onPlatformAppNetworkDeviceRecvEngageDatagram(final int deviceId, final int timeoutMs, final String jsonMetaData)
    {
        if(_appNetworkDeviceListener == null)
        {
            return null;
        }
        else
        {
            return _appNetworkDeviceListener.onAppNetworkDeviceRecvEngageDatagram(deviceId, timeoutMs, jsonMetaData);
        }
    }

    @Keep
    private int onPlatformAppNetworkDeviceSendEngageDatagram(final int deviceId, final EngageDatagram dg, final int timeoutMs, final String jsonMetaData)
    {
        if(_appNetworkDeviceListener == null)
        {
            return -1;
        }
        else
        {
            return _appNetworkDeviceListener.onAppNetworkDeviceSendEngageDatagram(deviceId, dg, timeoutMs, jsonMetaData);    
        }        
    }    


    // Other
    @Keep
    private int setupAndroidAudio(final String jsonMetaData)
    {
        shimLayerInternalLogging_D(TAG, "setupAndroidAudio");

        AudioManager am = (AudioManager) _appContext.getSystemService(Context.AUDIO_SERVICE);
        am.setMode(AudioManager.MODE_IN_COMMUNICATION);
        return 0;
    }


    @Keep
    private int cleanupAndroidAudio(final String jsonMetaData)
    {
        shimLayerInternalLogging_D(TAG, "cleanupAndroidAudio");

        // Nothing for now
        return 0;
    }


    @Keep
    private int getAndroidAudioSessionId()
    {
        // NOTE: Its not necessary to protect _audioSessionId as the Android audio functions
        // here are always called from the Engine's main thread and therefore serialized.        
        if(_audioSessionId == 0)
        {
            AudioManager am = (AudioManager) _appContext.getSystemService(Context.AUDIO_SERVICE);
            am.setMode(AudioManager.MODE_IN_COMMUNICATION);
            _audioSessionId = am.generateAudioSessionId();

            shimLayerInternalLogging_D(TAG, "getAndroidAudioSessionId setting the mode and generated a session id of " + _audioSessionId);
        }
        else
        {
            shimLayerInternalLogging_D(TAG, "getAndroidAudioSessionId returning a session ID of " + _audioSessionId);
        }
        
        return _audioSessionId;
    }

    // Security
    private static String convertToPEM(X509Certificate cert) 
    {
        try 
        {
            byte[] encoded = cert.getEncoded();
            String base64Cert = Base64.encodeToString(encoded, Base64.NO_WRAP);

            StringBuilder pemBuilder = new StringBuilder();
            pemBuilder.append("-----BEGIN CERTIFICATE-----\n");

            for (int i = 0; i < base64Cert.length(); i += 64) 
            {
                int endIndex = Math.min(i + 64, base64Cert.length());
                pemBuilder.append(base64Cert.substring(i, endIndex));
                pemBuilder.append("\n");
            }

            pemBuilder.append("-----END CERTIFICATE-----");

            return pemBuilder.toString();
        } 
        catch (CertificateException e) 
        {
            //shimLayerInternalLogging_E(TAG, "Failed to convert certificate to PEM format: " + e.toString());
            return null;
        }
    }

    @Keep
    private String[] onPlatformGetTrustedRootCertificates()
    {
        List<String> pemCerts = new ArrayList<>();

        try {
            KeyStore trustStore = KeyStore.getInstance("AndroidCAStore");
            trustStore.load(null, null);

            Enumeration<String> aliases = trustStore.aliases();
            int numLoaded = 0;

            while (aliases.hasMoreElements()) 
            {
                String alias = aliases.nextElement();

                try 
                {
                    X509Certificate cert = (X509Certificate) trustStore.getCertificate(alias);

                    if (cert != null) 
                    {
                        String pemCert = convertToPEM(cert);
                        if (pemCert != null) 
                        {
                            pemCerts.add(pemCert);
                            numLoaded++;
                            //shimLayerInternalLogging_D(TAG, "Loaded root CA certificate: " + cert.getSubjectDN().getName());
                        }
                    }
                } 
                catch (KeyStoreException e) 
                {
                    shimLayerInternalLogging_W(TAG, "Failed to retrieve certificate for alias: " + alias + ", " + e.toString());
                }
            }

            shimLayerInternalLogging_I(TAG, "Successfully loaded " + numLoaded + " root CA certificates");
        } 
        catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) 
        {
            shimLayerInternalLogging_E(TAG, "Failed to load system root CA certificates, " + e.toString());
        }

        return pemCerts.toArray(new String[0]);
    }


    // Event callbacks
    @Keep
    private void onEngineStarted(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineStarted(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onEngineStartFailed(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineStartFailed(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onEngineAudioDevicesRefreshed(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineAudioDevicesRefreshed(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onEngineGroupByGroupPcmPowerLevels(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineGroupByGroupPcmPowerLevels(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onEngineAudioDeviceEvent(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineAudioDeviceEvent(eventExtraJson);
                }
            }
        });
    }    

    @Keep
    private void onEngineStopped(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IEngineListener listener : _engineListeners)
                {
                    listener.onEngineStopped(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onRpLeafPausingConnectionAttempt(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IRallypointListener listener : _rallypointListeners)
                {
                    listener.onRallypointPausingConnectionAttempt(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onRpLeafConnecting(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IRallypointListener listener : _rallypointListeners)
                {
                    listener.onRallypointConnecting(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onRpLeafConnected(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IRallypointListener listener : _rallypointListeners)
                {
                    listener.onRallypointConnected(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onRpLeafDisconnected(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IRallypointListener listener : _rallypointListeners)
                {
                    listener.onRallypointDisconnected(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    public void onRpLeafRoundtripReport(final String id, final long rtMs, final long rtQualityRating, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IRallypointListener listener : _rallypointListeners)
                {
                    listener.onRallypointRoundtripReport(id, rtMs, rtQualityRating, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupCreated(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupCreated(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupCreateFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupCreateFailed(id, eventExtraJson);
                }
            }
        });
    }


    @Keep
    private void onGroupDeleted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupDeleted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupConnected(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupConnected(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupConnectFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupConnectFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupDisconnected(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupDisconnected(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupJoined(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupJoined(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupJoinFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupJoinFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupLeft(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupLeft(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupMemberCountChanged(final String id, final long count, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupMemberCountChanged(id, count, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxStarted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxStarted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxEnded(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxEnded(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxMuted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxMuted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxUnmuted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxUnmuted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxMuted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxMuted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxUnmuted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxUnmuted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxStarted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxStarted(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxEnded(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxEnded(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTxUsurpedByPriority(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTxUsurpedByPriority(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupMaxTxTimeExceeded(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupMaxTxTimeExceeded(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxSpeakersChanged(final String id, final String groupTalkerJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxSpeakersChanged(id, groupTalkerJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupNodeDiscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupNodeDiscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupNodeRediscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupNodeRediscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupNodeUndiscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupNodeUndiscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupAssetDiscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupAssetDiscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupAssetRediscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupAssetRediscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupAssetUndiscovered(final String id, final String nodeJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupAssetUndiscovered(id, nodeJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupBlobSent(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupBlobSent(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupBlobSendFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupBlobSendFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupBlobReceived(final String id, final String blobInfoJson, final byte[] blob, final long blobSize, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupBlobReceived(id, blobInfoJson, blob, blobSize, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRtpSent(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRtpSent(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRtpSendFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRtpSendFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRtpReceived(final String id, final String rtpHeaderJson, final byte[] payload, final long payloadSize, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRtpReceived(id, rtpHeaderJson, payload, payloadSize, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRawSent(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRawSent(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRawSendFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRawSendFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    void onGroupRawReceived(final String id, final byte[] raw, final long rawSize, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRawReceived(id, raw, rawSize, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupReconfigured(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupReconfigured(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupReconfigurationFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupReconfigurationFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onLicenseChanged(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (ILicenseListener listener : _licenseListeners)
                {
                    listener.onLicenseChanged(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onLicenseExpired(final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (ILicenseListener listener : _licenseListeners)
                {
                    listener.onLicenseExpired(eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onLicenseExpiring(final String secondsLeft, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                double doubleSecondsLeft;
                doubleSecondsLeft = Double.parseDouble(secondsLeft);
                for(ILicenseListener listener :_licenseListeners)
                {
                    listener.onLicenseExpiring(doubleSecondsLeft, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineEventStarted(final String id, final String eventJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineEventStarted(id, eventJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineEventUpdated(final String id, final String eventJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineEventUpdated(id, eventJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineEventEnded(final String id, final String eventJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineEventEnded(id, eventJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineReport(final String id, final String reportJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineReport(id, reportJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineReportFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineReportFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupTimelineGroomed(final String id, final String eventListJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupTimelineGroomed(id, eventListJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupHealthReport(final String id, final String healthReportJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupHealthReport(id, healthReportJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupHealthReportFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupHealthReportFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupStatsReport(final String id, final String statsReportJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupStatsReport(id, statsReportJson, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupStatsReportFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupStatsReportFailed(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxVolumeChanged(final String id, final int leftLevelPerc, final int rightLevelPerc, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxVolumeChanged(id, leftLevelPerc, rightLevelPerc, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onGroupRxDtmf(final String id, final String dtmfJson, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IGroupListener listener : _groupListeners)
                {
                    listener.onGroupRxDtmf(id, dtmfJson, eventExtraJson);
                }
            }
        });
    }    

    //--------------
    private void onBridgeCreated(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IBridgeListener listener : _bridgeListeners)
                {
                    listener.onBridgeCreated(id, eventExtraJson);
                }
            }
        });
    }

    @Keep
    private void onBridgeCreateFailed(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IBridgeListener listener : _bridgeListeners)
                {
                    listener.onBridgeCreateFailed(id, eventExtraJson);
                }
            }
        });
    }


    @Keep
    private void onBridgeDeleted(final String id, final String eventExtraJson)
    {
        _handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                for (IBridgeListener listener : _bridgeListeners)
                {
                    listener.onBridgeDeleted(id, eventExtraJson);
                }
            }
        });
    }
}
