/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext

/**
 * [DefaultEglCore] is an implementation of [EglCore] which uses EGL14 and OpenGLES2.
 * OpenGLES3 has incompatibilities with AmazonChimeSDKMedia library.
 */
class DefaultEglCore(
    private val releaseCallback: Runnable? = null,
    sharedContext: EGLContext = EGL14.EGL_NO_CONTEXT
) : EglCore {
    override var eglContext = EGL14.EGL_NO_CONTEXT
    override var eglSurface = EGL14.EGL_NO_SURFACE
    override var eglDisplay = EGL14.EGL_NO_DISPLAY
    override lateinit var eglConfig: EGLConfig

    init {
        eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
        if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw RuntimeException("Unable to get EGL14 display")
        }
        val version = IntArray(2)
        if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
            eglDisplay = null
            throw RuntimeException("Unable to initialize EGL14")
        }

        eglConfig = getConfig()

        // Use version 2 for compatibility with AmazonChimeSDKMedia library
        val attributeList = intArrayOf(
            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL14.EGL_NONE
        )
        eglContext = EGL14.eglCreateContext(
            eglDisplay, eglConfig, sharedContext,
            attributeList, 0
        )

        // Confirm with query.
        val values = IntArray(1)
        if (!EGL14.eglQueryContext(
                eglDisplay, eglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
                values, 0
            )
        ) {
            throw RuntimeException("Failed to query context")
        }
    }

    override fun release() {
        if (eglSurface != EGL14.EGL_NO_SURFACE) {
            EGL14.eglDestroySurface(eglDisplay, eglSurface)
            eglSurface = EGL14.EGL_NO_SURFACE
        }

        if (eglDisplay != EGL14.EGL_NO_DISPLAY) {
            // Android is unusual in that it uses a reference-counted EGLDisplay. So for
            // every eglInitialize() we need an eglTerminate().
            EGL14.eglMakeCurrent(
                eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT
            )
            EGL14.eglDestroyContext(eglDisplay, eglContext)
            EGL14.eglReleaseThread()
            EGL14.eglTerminate(eglDisplay)
        }
        eglDisplay = EGL14.EGL_NO_DISPLAY
        eglContext = EGL14.EGL_NO_CONTEXT

        releaseCallback?.run()
    }

    private fun getConfig(): EGLConfig {
        val attributeList = intArrayOf(
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, // ES2 for compatability with AmazonChimeSDKMedia library
            EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT, // On by default
            EGL14.EGL_NONE, 0,
            EGL14.EGL_NONE
        )

        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = IntArray(1)
        if (!EGL14.eglChooseConfig(
                eglDisplay, attributeList, 0, configs, 0, configs.size,
                numConfigs, 0
            )
        ) {
            throw RuntimeException("Failed to choose config")
        }
        return configs[0] ?: throw RuntimeException("Config was null")
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.opengl.EGL14
import android.opengl.EGLContext
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.ShareEglLock
import com.amazonaws.services.chime.sdk.meetings.utils.RefCountDelegate

/**
 * [DefaultEglCoreFactory] will create a root [EglCore] lazily if no shared context is provided.
 * It will track all child [EglCore] objects and release the root core if all child cores are released.
 */
class DefaultEglCoreFactory(private var sharedContext: EGLContext = EGL14.EGL_NO_CONTEXT) :
    EglCoreFactory {
    private var rootEglCore: EglCore? = null
    private var refCountDelegate: RefCountDelegate? = null

    override fun createEglCore(): EglCore {
        synchronized(ShareEglLock.Lock) {
            if (rootEglCore == null && sharedContext == EGL14.EGL_NO_CONTEXT) {
                refCountDelegate = RefCountDelegate(Runnable { release() })
                rootEglCore = DefaultEglCore().also {
                    sharedContext = it.eglContext
                }
            } else {
                refCountDelegate?.retain()
            }
            return DefaultEglCore(Runnable { onEglCoreReleased() }, sharedContext)
        }
    }

    private fun onEglCoreReleased() {
        refCountDelegate?.release()
    }

    private fun release() {
        synchronized(ShareEglLock.Lock) {
            if (rootEglCore != null) {
                rootEglCore?.release()
                rootEglCore = null
                sharedContext = EGL14.EGL_NO_CONTEXT
                refCountDelegate = null
            }
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLDisplay
import android.opengl.EGLSurface

/**
 * [EglCore] is an interface for containing all EGL state in one component. In the future it may contain additional helper methods.
 */
interface EglCore {
    /**
     * A [EGLContext] which was created with [eglDisplay] and [eglConfig], may or may not be the current context on the thread, users
     * must call [EGL14.eglMakeCurrent] after creating a valid current surface. This may be passed to other components to share the context.
     */
    val eglContext: EGLContext

    /**
     * Current initialized [EGLDisplay]
     */
    val eglDisplay: EGLDisplay

    /**
     * Current used [EGLConfig]
     */
    val eglConfig: EGLConfig

    /**
     * Current [EGLSurface]. Will likely be [EGL14.EGL_NO_SURFACE] on init. As [EglCore] does not include helper functions
     * users must create this value themselves, which is why it is defined as `var`
     */
    var eglSurface: EGLSurface

    /**
     * Discards all resources held by this class, notably the EGL context. This must be
     * called from the thread where the context was created.
     *
     * On completion, no context will be current.
     */
    fun release()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

/**
 * [EglCoreFactory] is an factory interface for creating new [EglCore] objects, possible using shared state
 */
interface EglCoreFactory {
    /**
     * Create a new [EglCore] object
     *
     * @return [EglCore] - Newly created and initialized [EglCore] object
     */
    fun createEglCore(): EglCore
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoRenderView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer

/**
 * [EglVideoRenderView] is a [VideoRenderView] which requires EGL initialization to render [VideoFrameTextureBuffer] buffers.
 * The [VideoTileController] should automatically manage ([init] and [release]) any bound tiles, but if it
 * is desired to use a view outside of the controller (e.g. in pre-meeting device selection), users will
 * need to call [init] and [release] themselves
 */
interface EglVideoRenderView : VideoRenderView {
    /**
     * Initialize view with factory to create [EglCore] objects to hold/share EGL state
     *
     * @param eglCoreFactory: [EglCoreFactory] - Factory to create [EglCore] objects to hold EGL state
     */
    fun init(eglCoreFactory: EglCoreFactory)

    /**
     * Deallocate any state or resources held by this object
     */
    fun release()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.content.Context
import android.graphics.Point
import android.opengl.EGL14
import android.util.AttributeSet
import android.view.SurfaceHolder
import android.view.SurfaceView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoRotation
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoScalingType
import com.amazonaws.services.chime.sdk.meetings.internal.utils.VideoLayoutMeasure
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.DefaultEglRenderer
import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
 * [SurfaceRenderView] is an implementation of [EglVideoRenderView] which uses EGL14 and OpenGLES2
 * to draw any incoming video buffer types to the surface provided by the inherited [SurfaceView].
 *
 * Note that since most [SurfaceRenderView] objects will not be constructed in code, builders must
 * pass in the [Logger] directly before initialization by setting [logger]
 */
open class SurfaceRenderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : SurfaceView(context, attrs, defStyle), SurfaceHolder.Callback,
        EglVideoRenderView {
    // Accessed only on the main thread.
    private var rotatedFrameWidth = 0
    private var rotatedFrameHeight = 0
    private var frameRotation = VideoRotation.Rotation0

    private var surfaceWidth = 0
    private var surfaceHeight = 0

    // This helper class is used to determine the value to set by setMeasuredDimension in onMeasure
    // (Required for View implementations) which determines the actual size of the view
    private val videoLayoutMeasure: VideoLayoutMeasure = VideoLayoutMeasure()

    // Lazy so we can allow builders to set their own loggers using self.logger
    // If no logger is passed in we will fallback on ConsoleLogger
    private val renderer by lazy {
        DefaultEglRenderer(logger)
    }

    /**
     * If true, rendered frames will be mirrored across the vertical axis
     */
    var mirror: Boolean = false
        set(value) {
            logger.debug(TAG, "Setting mirror from $field to $value")
            renderer.mirror = value
            field = value
        }

    /**
     * [VideoScalingType] used to render on this view.  May impact cropping.
     */
    var scalingType: VideoScalingType = VideoScalingType.AspectFill
        set(value) {
            logger.debug(TAG, "Setting scaling type from $field to $value")
            videoLayoutMeasure.scalingType = value
            field = value
        }

    /**
     * Enables fixed size for the surface. This provides better performance but might be buggy on some
     * devices, for example on Galaxy S9, [EGL14.eglQuerySurface] will not return update values until
     * after the first [EGL14.eglSwapBuffers] call, and will lead to cropping or black boxing on resolution
     * changes. By default this is turned off.
     */
    var hardwareScaling: Boolean = false
        set(value) {
            logger.debug(TAG, "Setting hardware scaling from $field to $value")
            field = value
        }

    // Public so it can be set, since most users will not be using constructor directly
    var logger: Logger = ConsoleLogger()

    private val TAG = "SurfaceRenderView"

    init {
        holder?.addCallback(this)
    }

    override fun init(eglCoreFactory: EglCoreFactory) {
        rotatedFrameWidth = 0
        rotatedFrameHeight = 0

        logger.info(TAG, "Initializing render view")
        renderer.init(eglCoreFactory)
    }

    override fun release() {
        logger.info(TAG, "Releasing render view")
        renderer.release()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        logger.info(TAG, "Surface destroyed, releasing EGL surface")
        renderer.releaseEglSurface()
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        updateSurfaceSize()

        // Create the EGL surface and set it as current
        logger.info(TAG, "Surface created, creating EGL surface with resource")
        renderer.createEglSurface(holder.surface)
    }

    override fun onMeasure(widthSpec: Int, heightSpec: Int) {
        val size: Point =
                videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight)
        logger.debug(TAG, "Setting measured dimensions ${size.x}x${size.y}")
        setMeasuredDimension(size.x, size.y)
    }

    override fun onLayout(
        changed: Boolean,
        left: Int,
        top: Int,
        right: Int,
        bottom: Int
    ) {
        renderer.aspectRatio = ((right - left) / (bottom - top).toFloat())
        updateSurfaceSize()
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        // Update internal sizing and layout if frame size changes
        if (rotatedFrameWidth != frame.getRotatedWidth() ||
                rotatedFrameHeight != frame.getRotatedHeight() ||
                frameRotation != frame.rotation
        ) {
            rotatedFrameWidth = frame.getRotatedWidth()
            rotatedFrameHeight = frame.getRotatedHeight()
            frameRotation = frame.rotation

            logger.info(TAG, "Video frame rotated size changed to ${rotatedFrameWidth}x$rotatedFrameHeight with rotation $frameRotation")

            CoroutineScope(Dispatchers.Main).launch {
                updateSurfaceSize()
                requestLayout()
            }
        }
        renderer.onVideoFrameReceived(frame)
    }

    private fun updateSurfaceSize() {
        if (hardwareScaling && rotatedFrameWidth != 0 && rotatedFrameHeight != 0 && width != 0 && height != 0) {
            val layoutAspectRatio = width / height.toFloat()
            val frameAspectRatio: Float =
                    rotatedFrameWidth.toFloat() / rotatedFrameHeight
            val drawnFrameWidth: Int
            val drawnFrameHeight: Int
            if (frameAspectRatio > layoutAspectRatio) {
                drawnFrameWidth = ((rotatedFrameHeight * layoutAspectRatio).toInt())
                drawnFrameHeight = rotatedFrameHeight
            } else {
                drawnFrameWidth = rotatedFrameWidth
                drawnFrameHeight = ((rotatedFrameWidth / layoutAspectRatio).toInt())
            }
            // Aspect ratio of the drawn frame and the view is the same.
            val width = min(width, drawnFrameWidth)
            val height = min(height, drawnFrameHeight)

            logger.info(TAG, "Updating surface size frame size: ${rotatedFrameWidth}x$rotatedFrameHeight, " +
                    "requested surface size: ${width}x$height, old surface size: ${surfaceWidth}x$surfaceHeight")
            if (width != surfaceWidth || height != surfaceHeight) {
                surfaceWidth = width
                surfaceHeight = height
                holder.setFixedSize(width, height)
            }
        } else {
            surfaceHeight = 0
            surfaceWidth = surfaceHeight
            holder?.setSizeFromLayout()
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.content.Context
import android.graphics.Point
import android.graphics.SurfaceTexture
import android.util.AttributeSet
import android.view.TextureView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoRotation
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoScalingType
import com.amazonaws.services.chime.sdk.meetings.internal.utils.VideoLayoutMeasure
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.DefaultEglRenderer
import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
 * [TextureRenderView] is an implementation of [EglVideoRenderView] which uses EGL14 and OpenGLES2
 * to draw any incoming video buffer types to the surface provided by the inherited [TextureView]
 *
 * Note that since most [TextureRenderView] objects will not be constructed in code, builders must
 * pass in the [Logger] directly before initialization by setting [logger]
 */
open class TextureRenderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : TextureView(context, attrs, defStyle), TextureView.SurfaceTextureListener,
        EglVideoRenderView {
    // Accessed only on the main thread.
    private var rotatedFrameWidth = 0
    private var rotatedFrameHeight = 0
    private var frameRotation = VideoRotation.Rotation0

    // This helper class is used to determine the value to set by setMeasuredDimension in onMeasure
    // (Required for View implementations) which determines the actual size of the view
    private val videoLayoutMeasure: VideoLayoutMeasure = VideoLayoutMeasure()

    private val renderer by lazy {
        // Lazy so we can allow builders to set their own loggers
        DefaultEglRenderer(logger)
    }

    // Public so it can be set, since most users will not be using constructor directly
    var logger: Logger = ConsoleLogger()

    private val TAG = "TextureRenderView"

    /**
     * If true, rendered frames will be mirrored across the vertical axis
     */
    var mirror: Boolean = false
        set(value) {
            logger.debug(TAG, "Setting mirror from $field to $value")
            renderer.mirror = value
            field = value
        }

    /**
     * [VideoScalingType] used to render on this view.  May impact cropping.
     */
    var scalingType: VideoScalingType = VideoScalingType.AspectFill
        set(value) {
            logger.debug(TAG, "Setting scaling type from $field to $value")
            videoLayoutMeasure.scalingType = value
            field = value
        }

    init {
        surfaceTextureListener = this
    }

    override fun init(eglCoreFactory: EglCoreFactory) {
        rotatedFrameWidth = 0
        rotatedFrameHeight = 0

        logger.info(TAG, "Initializing render view")
        renderer.init(eglCoreFactory)
    }

    override fun release() {
        logger.info(TAG, "Releasing render view")
        renderer.release()
    }

    override fun onMeasure(widthSpec: Int, heightSpec: Int) {
        val size: Point =
                videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight)
        logger.debug(TAG, "Setting measured dimensions ${size.x}x${size.y}")
        setMeasuredDimension(size.x, size.y)
    }

    override fun onLayout(
        changed: Boolean,
        left: Int,
        top: Int,
        right: Int,
        bottom: Int
    ) {
        renderer.aspectRatio = ((right - left) / (bottom - top).toFloat())
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        // Update internal sizing and layout if frame size changes
        if (rotatedFrameWidth != frame.getRotatedWidth() ||
                rotatedFrameHeight != frame.getRotatedHeight() ||
                frameRotation != frame.rotation
        ) {
            rotatedFrameWidth = frame.getRotatedWidth()
            rotatedFrameHeight = frame.getRotatedHeight()
            frameRotation = frame.rotation
            logger.info(TAG, "Video frame rotated size changed to ${rotatedFrameWidth}x$rotatedFrameHeight")

            CoroutineScope(Dispatchers.Main).launch {
                requestLayout()
            }
        }

        renderer.onVideoFrameReceived(frame)
    }

    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
        return
    }

    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
        return
    }

    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
        logger.info(TAG, "Surface destroyed, releasing EGL surface")
        renderer.releaseEglSurface()
        return true
    }

    override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
        // Create the EGL surface and set it as current
        surface.let {
            logger.info(TAG, "Surface created, creating EGL surface with resource")
            renderer.createEglSurface(it)
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.device

import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEventName
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientState
import com.amazonaws.services.chime.sdk.meetings.internal.audio.DefaultAudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ConcurrentSet
import com.amazonaws.services.chime.sdk.meetings.internal.utils.ObserverUtils
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.xodee.client.audio.audioclient.AudioClient

class DefaultDeviceController(
    private val context: Context,
    private val audioClientController: AudioClientController,
    private val videoClientController: VideoClientController,
    private val eventAnalyticsController: EventAnalyticsController,
    private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager,
    private val buildVersion: Int = Build.VERSION.SDK_INT
) : DeviceController {
    private val deviceChangeObservers = ConcurrentSet.createConcurrentSet<DeviceChangeObserver>()

    // TODO: remove code blocks for lower API level after the minimum SDK version becomes 23
    private val AUDIO_MANAGER_API_LEVEL = 23

    private var receiver: BroadcastReceiver? = null

    private var audioDeviceCallback: AudioDeviceCallback? = null

    init {
        @SuppressLint("NewApi")
        if (buildVersion >= AUDIO_MANAGER_API_LEVEL) {
            audioDeviceCallback = object : AudioDeviceCallback() {
                override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
                    notifyAudioDeviceChange()
                }

                override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
                    notifyAudioDeviceChange()
                }
            }
            audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
        } else {
            receiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    // There is gap between notification and audioManager recognizing bluetooth devices
                    if (intent?.action == BluetoothDevice.ACTION_ACL_CONNECTED) {
                        Thread.sleep(1000)
                    }
                    notifyAudioDeviceChange()
                }
            }
            context.registerReceiver(receiver, IntentFilter(Intent.ACTION_HEADSET_PLUG))
            context.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED))
            context.registerReceiver(
                receiver, IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)
            )
        }
    }

    override fun listAudioDevices(): List<MediaDevice> {
        @SuppressLint("NewApi")
        if (buildVersion >= AUDIO_MANAGER_API_LEVEL) {
            var isWiredHeadsetOn = false
            var isHandsetAvailable = false
            val handsetDevicesInfo = setOf(
                AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
                AudioDeviceInfo.TYPE_TELEPHONY
            )

            val audioDevices = mutableListOf<MediaDevice>()
            var wiredDeviceCount = 0
            for (device in audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
                // System will select wired headset over receiver
                // so we want to filter receiver out when wired headset is connected
                if (device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
                    device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
                    device.type == AudioDeviceInfo.TYPE_USB_HEADSET
                ) {
                    isWiredHeadsetOn = true
                    wiredDeviceCount++
                }

                // Return only one handset device to avoid confusion
                if (handsetDevicesInfo.contains(device.type)) {
                    if (isHandsetAvailable) continue
                    else {
                        isHandsetAvailable = true
                    }
                }

                if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) continue

                audioDevices.add(
                    MediaDevice(
                        "${device.productName} (${getReadableType(device.type)})",
                        MediaDeviceType.fromAudioDeviceInfo(
                            device.type
                        )
                    )
                )
            }

            // It doesn't look like Android can switch between two wired connection, so we'll assume WIRED_HEADSET
            // is where audio is routed.
            if (wiredDeviceCount > 1) audioDevices.removeIf { it.type == MediaDeviceType.AUDIO_USB_HEADSET }
            return if (isWiredHeadsetOn) audioDevices.filter { it.type != MediaDeviceType.AUDIO_HANDSET } else audioDevices
        } else {
            val res = mutableListOf<MediaDevice>()
            res.add(
                MediaDevice(
                    getReadableType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER),
                    MediaDeviceType.AUDIO_BUILTIN_SPEAKER
                )
            )
            if (audioManager.isWiredHeadsetOn) {
                res.add(
                    MediaDevice(
                        getReadableType(AudioDeviceInfo.TYPE_WIRED_HEADSET),
                        MediaDeviceType.AUDIO_WIRED_HEADSET
                    )
                )
            } else {
                res.add(
                    MediaDevice(
                        getReadableType(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE),
                        MediaDeviceType.AUDIO_HANDSET
                    )
                )
            }
            if (audioManager.isBluetoothScoOn) {
                res.add(
                    MediaDevice(
                        getReadableType(AudioDeviceInfo.TYPE_BLUETOOTH_SCO),
                        MediaDeviceType.AUDIO_BLUETOOTH
                    )
                )
            }
            return res
        }
    }

    override fun chooseAudioDevice(mediaDevice: MediaDevice) {
        if (DefaultAudioClientController.audioClientState != AudioClientState.STARTED) {
            return
        }
        setupAudioDevice(mediaDevice.type)
        val route = when (mediaDevice.type) {
            MediaDeviceType.AUDIO_BUILTIN_SPEAKER -> AudioClient.SPK_STREAM_ROUTE_SPEAKER
            MediaDeviceType.AUDIO_BLUETOOTH -> AudioClient.SPK_STREAM_ROUTE_BT_AUDIO
            MediaDeviceType.AUDIO_WIRED_HEADSET -> AudioClient.SPK_STREAM_ROUTE_HEADSET
            MediaDeviceType.AUDIO_USB_HEADSET -> AudioClient.SPK_STREAM_ROUTE_HEADSET
            else -> AudioClient.SPK_STREAM_ROUTE_RECEIVER
        }

        val selected = audioClientController.setRoute(route)
        if (selected) {
            eventAnalyticsController.pushHistory(MeetingHistoryEventName.audioInputSelected)
        }
    }

    @RequiresApi(Build.VERSION_CODES.N)
    override fun getActiveAudioDevice(): MediaDevice? {
        if (buildVersion >= Build.VERSION_CODES.N) {
            if (audioManager.activeRecordingConfigurations.isNotEmpty()) {
                val device =
                    audioManager.activeRecordingConfigurations.firstOrNull { config -> config.audioDevice != null }?.audioDevice
                if (device != null) {
                    val type = device.type
                    var mediaDeviceType: MediaDeviceType = MediaDeviceType.fromAudioDeviceInfo(type)
                    if (type == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
                        // Built-in mic has two case speaker, built-in receiver
                        mediaDeviceType = if (audioManager.isSpeakerphoneOn) {
                            MediaDeviceType.fromAudioDeviceInfo(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
                        } else {
                            MediaDeviceType.fromAudioDeviceInfo(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
                        }
                    }
                    return listAudioDevices().firstOrNull {
                        it.type == mediaDeviceType
                    }
                }

                // Some android devices doesn't have audio device for speaker
                if (audioManager.isSpeakerphoneOn) return listAudioDevices().firstOrNull {
                    it.type == MediaDeviceType.AUDIO_BUILTIN_SPEAKER
                }
            }
        }
        return null
    }

    private fun setupAudioDevice(type: MediaDeviceType) {
        when (type) {
            MediaDeviceType.AUDIO_BUILTIN_SPEAKER ->
                audioManager.apply {
                    // Sometimes stopBluetoothSco makes isSpeakerphoneOn to be false
                    // calling it before isSpeakerphoneOn
                    stopBluetoothSco()
                    mode = AudioManager.MODE_IN_COMMUNICATION
                    isBluetoothScoOn = false
                    isSpeakerphoneOn = true
                }
            MediaDeviceType.AUDIO_BLUETOOTH ->
                audioManager.apply {
                    mode = AudioManager.MODE_IN_COMMUNICATION
                    isSpeakerphoneOn = false
                    startBluetoothSco()
                    isBluetoothScoOn = true
                }
            else ->
                audioManager.apply {
                    stopBluetoothSco()
                    isBluetoothScoOn = false
                    mode = AudioManager.MODE_IN_COMMUNICATION
                    isSpeakerphoneOn = false
                }
        }
    }

    private fun getReadableType(type: Int): String {
        return when (type) {
            AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
            AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Speaker"
            AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Wired Headphone"
            AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
            AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
            AudioDeviceInfo.TYPE_TELEPHONY -> "Handset"
            AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
            AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
            else -> "Unknown (AudioDeviceInfo: $type)"
        }
    }

    override fun getActiveCamera(): MediaDevice? {
        return videoClientController.getActiveCamera()
    }

    override fun switchCamera() {
        videoClientController.switchCamera()
    }

    override fun addDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceChangeObservers.add(observer)
    }

    override fun removeDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceChangeObservers.remove(observer)
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun notifyAudioDeviceChange() {
        ObserverUtils.notifyObserverOnMainThread(deviceChangeObservers) {
            it.onAudioDeviceChanged(
                listAudioDevices()
            )
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.device

/**
 * [DeviceChangeObserver] listens audio device changes.
 *
 * Note: all callbacks will be called on main thread.
 */
interface DeviceChangeObserver {
    /**
     * Called when audio devices are changed.
     *
     * Note: this callback will be called on main thread.
     *
     * @param freshAudioDeviceList: List<[MediaDevice]> - An updated list of audio devices.
     */
    fun onAudioDeviceChanged(freshAudioDeviceList: List<MediaDevice>)
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.device

import android.os.Build
import androidx.annotation.RequiresApi
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoControllerFacade

/**
 * [DeviceController] keeps track of the devices being used for audio device
 * (e.g. built-in speaker), video input (e.g. camera)).
 * The list functions return [MediaDevice] objects.
 * Changes in device availability are broadcast to any registered
 * [DeviceChangeObserver].
 */
interface DeviceController {
    /**
     * Lists currently available audio devices.
     *
     * Note: If there are both USB and earphone jack connected. The device will only show earphone.
     *
     * @return a list of currently available audio devices.
     */
    fun listAudioDevices(): List<MediaDevice>

    /**
     * Selects an audio device to use.
     *
     * Note: [chooseAudioDevice] is no-op when audio client is not started.
     *
     * @param mediaDevice the audio device selected to use.
     */
    fun chooseAudioDevice(mediaDevice: MediaDevice)

    /**
     * Get the active input/output audio device in the meeting, return null if there isn't any.
     *
     * NOTE: This requires Android API 24 and above
     *
     * @return the active local audio device
     */
    @RequiresApi(Build.VERSION_CODES.N)
    fun getActiveAudioDevice(): MediaDevice?

    /**
     * Adds an observer to receive callbacks about device changes.
     *
     * @param observer device change observer
     */
    fun addDeviceChangeObserver(observer: DeviceChangeObserver)

    /**
     * Removes an observer to stop receiving callbacks about device changes.
     *
     * @param observer device change observer
     */
    fun removeDeviceChangeObserver(observer: DeviceChangeObserver)

    /**
     * Get the currently active camera, if any. This will return null if using a custom source,
     * e.g. one passed in via [AudioVideoControllerFacade.startLocalVideo]
     *
     * @return [MediaDevice] - Information about the current active device used for video.
     */
    fun getActiveCamera(): MediaDevice?

    /**
     * Switches the currently active camera. This will no-op if using a custom source,
     * e.g. one passed in via [AudioVideoControllerFacade.startLocalVideo]
     */
    fun switchCamera()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.device

import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.media.AudioDeviceInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.VideoCaptureFormat

/**
 * Media device with its info.
 *
 * @property label: String - human readable string describing the device.
 * @property type: [MediaDeviceType] - media device type
 * @property id: String - Unique ID, if applicable
 */
data class MediaDevice(
    val label: String,
    val type: MediaDeviceType,
    val id: String? = null
) {
    val order: Int = when (type) {
        MediaDeviceType.AUDIO_BLUETOOTH -> 0
        MediaDeviceType.AUDIO_WIRED_HEADSET -> 1
        MediaDeviceType.AUDIO_USB_HEADSET -> 1
        MediaDeviceType.AUDIO_BUILTIN_SPEAKER -> 2
        MediaDeviceType.AUDIO_HANDSET -> 3
        MediaDeviceType.VIDEO_FRONT_CAMERA -> 4
        MediaDeviceType.VIDEO_BACK_CAMERA -> 5
        MediaDeviceType.VIDEO_EXTERNAL_CAMERA -> 6
        else -> 99
    }

    override fun toString(): String = label

    companion object {

        private val DEFAULT_MAX_VIDEO_FORMAT_FPS = 30

        /**
         * Lists currently available video devices.
         *
         * @param cameraManager: [CameraManager] - Camera manager to use for enumeration
         *
         * @return [List<MediaDevice>] - A list of currently available video devices.
         */
        fun listVideoDevices(cameraManager: CameraManager): List<MediaDevice> {
            return cameraManager.cameraIdList.map { id ->
                val characteristics = cameraManager.getCameraCharacteristics(id)
                characteristics.get(CameraCharacteristics.LENS_FACING)?.let {
                    val type = MediaDeviceType.fromCameraMetadata(it)
                    return@map MediaDevice("$id ($type)", type, id)
                }
                return@map MediaDevice("$id ($MediaDeviceType.OTHER)", MediaDeviceType.OTHER, id)
            }
        }

        /**
         * Lists currently available video devices.
         *
         * @param cameraManager: [CameraManager] - Camera manager to use for enumeration
         * @param mediaDevice: [MediaDevice] - Media device to inspect
         * @param maxVideoFps: Int - Desired max fps
         * @return [List<VideoCaptureFormat>] - A list of supported formats for the given device
         */
        fun listSupportedVideoCaptureFormats(
            cameraManager: CameraManager,
            mediaDevice: MediaDevice,
            maxVideoFps: Int = DEFAULT_MAX_VIDEO_FORMAT_FPS
        ): List<VideoCaptureFormat> {
            val characteristics = cameraManager.getCameraCharacteristics(mediaDevice.id ?: return emptyList())
            val fps = characteristics.get(
                CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
            )?.map { it.upper }?.filter { it <= maxVideoFps }?.max() ?: DEFAULT_MAX_VIDEO_FORMAT_FPS

            val streamMap =
                    characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                            ?: return emptyList()
            val nativeSizes = streamMap.getOutputSizes(SurfaceTexture::class.java)
                    ?: return emptyList()

            return nativeSizes.map { size -> VideoCaptureFormat(size.width, size.height, fps) }
        }
    }
}

/**
 * The media device's type (Ex: video front camera, video rear camera, audio bluetooth)
 */
enum class MediaDeviceType {
    AUDIO_BLUETOOTH,
    AUDIO_WIRED_HEADSET,
    AUDIO_USB_HEADSET,
    AUDIO_BUILTIN_SPEAKER,
    AUDIO_HANDSET,
    VIDEO_FRONT_CAMERA,
    VIDEO_BACK_CAMERA,
    VIDEO_EXTERNAL_CAMERA,
    OTHER;

    override fun toString(): String {
        return when (this) {
            AUDIO_BLUETOOTH -> "Bluetooth"
            AUDIO_WIRED_HEADSET -> "Wired Headset"
            AUDIO_USB_HEADSET -> "USB Headset"
            AUDIO_BUILTIN_SPEAKER -> "Builtin Speaker"
            AUDIO_HANDSET -> "Handset"
            VIDEO_FRONT_CAMERA -> "Front Camera"
            VIDEO_BACK_CAMERA -> "Back Camera"
            VIDEO_EXTERNAL_CAMERA -> "External Camera"
            OTHER -> "Other"
        }
    }

    companion object {
        fun fromAudioDeviceInfo(audioDeviceInfo: Int): MediaDeviceType {
            return when (audioDeviceInfo) {
                AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> AUDIO_BLUETOOTH
                AudioDeviceInfo.TYPE_WIRED_HEADSET,
                AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> AUDIO_WIRED_HEADSET
                AudioDeviceInfo.TYPE_USB_HEADSET -> AUDIO_USB_HEADSET
                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> AUDIO_BUILTIN_SPEAKER
                AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
                AudioDeviceInfo.TYPE_TELEPHONY -> AUDIO_HANDSET
                else -> OTHER
            }
        }

        fun fromCameraMetadata(cameraMetadata: Int): MediaDeviceType {
            return when (cameraMetadata) {
                CameraMetadata.LENS_FACING_FRONT -> VIDEO_FRONT_CAMERA
                CameraMetadata.LENS_FACING_BACK -> VIDEO_BACK_CAMERA
                CameraMetadata.LENS_FACING_EXTERNAL -> VIDEO_EXTERNAL_CAMERA
                else -> OTHER
            }
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import java.util.Timer
import java.util.TimerTask

class DefaultEventReporter constructor(
    private val ingestionConfiguration: IngestionConfiguration,
    private val eventBuffer: EventBuffer,
    private val logger: Logger,
    // isDaemon is false to kill when app gets killed.
    // This is to make sure timer doesn't continuously run after app is killed.
    private val timer: Timer = Timer("DefaultEventReporter", false)
) : EventReporter {
    private val TAG = "DefaultEventReporter"

    private var isStarted = false
    init {
        start()
    }

    override fun report(event: SDKEvent) {
        val updatedEvent = event.putAttributes(ingestionConfiguration.clientConfiguration.metadataAttributes)

        eventBuffer.add(updatedEvent)
    }

    override fun start() {
        if (isStarted || ingestionConfiguration.disabled) {
            return
        }

        isStarted = true

        try {
            timer.scheduleAtFixedRate(
                object : TimerTask() {
                    override fun run() = eventBuffer.process()
                },
                ingestionConfiguration.flushIntervalMs,
                ingestionConfiguration.flushIntervalMs
            )
        } catch (error: Error) {
            logger.error(TAG, "Failed to start timer for event buffer to process")
            isStarted = false
        }
    }

    override fun stop() {
        if (!isStarted) {
            return
        }

        timer.cancel()

        isStarted = false
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.internal.utils.DefaultBackOffRetry
import com.amazonaws.services.chime.sdk.meetings.internal.utils.HttpUtils
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.google.gson.Gson
import java.net.URL

class DefaultEventSender(
    private val ingestionConfiguration: IngestionConfiguration,
    private val logger: Logger
) : EventSender {
    private val TAG = "DefaultEventSender"
    private val gson = Gson()
    private val AUTHORIZATION_HEADER = "Authorization"
    private val BEARER = "Bearer"
    private val eventUrl: URL = URL(ingestionConfiguration.ingestionUrl)

    // 408: Request Timeout
    // 429: Too many request
    // 500: Internal Server Error
    // 502: Bad Gateway
    // 503: Service Unavailable
    // 504: Gateway timeout
    private val retryableCodeSet = setOf<Int>(
        408, 429, 500, 502, 503, 504
    )

    override suspend fun sendRecord(record: IngestionRecord): Boolean {
        return try {
            val body = gson.toJson(record)
            val response = HttpUtils.post(
                eventUrl,
                body,
                DefaultBackOffRetry(
                    ingestionConfiguration.retryCountLimit,
                    retryableStatusCodes = retryableCodeSet
                ),
                logger,
                mapOf(
                    AUTHORIZATION_HEADER to "$BEARER ${ingestionConfiguration.clientConfiguration.eventClientJoinToken}"
                )
            )
            if (response.httpException != null) {
                logger.error(TAG, "Failed to send ingestion record: $response")
                false
            } else { true }
        } catch (err: Exception) {
            logger.error(
                TAG,
                "Unable to send record ${err.localizedMessage}"
            )
            false
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.DirtyEventDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.DirtyMeetingEventItem
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.EventDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.IngestionEventConverter
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.MeetingEventItem
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import java.util.Calendar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class DefaultMeetingEventBuffer @JvmOverloads constructor(
    private val ingestionConfiguration: IngestionConfiguration,
    private val eventDao: EventDao,
    private val dirtyEventDao: DirtyEventDao,
    private val eventSender: EventSender,
    private val logger: Logger,
    private val eventScope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : EventBuffer {
    private val TAG = "DefaultMeetingEventBuffer"
    private val TWO_DAYS_IN_MILLISECONDS = 172_800_000

    private var isRunning = false

    init {
        // process dirty events when init
        processDirtyEvents()
    }

    override fun add(item: SDKEvent) {
        val meetingEventItem = MeetingEventItem(data = item)
        // Meeting event is also added in case of immediate send
        // due to the fact that people will likely close the app,
        // leading to possible loss of data. To make sure data is not lost,
        // we first add it to the database.
        eventDao.insertMeetingEvent(meetingEventItem)

        if (shouldProcessImmediately(item)) {
            eventScope.launch {
                processEvents(listOf(meetingEventItem))
            }
        }
    }

    override fun process() {
        eventScope.launch {
            if (isRunning) {
                return@launch
            }
            isRunning = true

            val meetingEventItems = eventDao.listMeetingEventItems(ingestionConfiguration.flushSize)
            processEvents(meetingEventItems)
            isRunning = false
        }
    }

    private suspend fun processEvents(eventItems: List<MeetingEventItem>) {
        /*
         * This will do few things
         * 1. Send data if not empty
         * 2. Retry if it is failure
         * 3. If success, remove from event
         * 4. If failure, add it to dirty event
         * 5. Remove ids from event table
         */
        try {
            if (eventItems.isEmpty()) {
                return
            }

            val eventRecord =
                IngestionEventConverter.fromMeetingEventItems(eventItems, ingestionConfiguration)

            val idsToRemove = eventItems.map { it.id }.toList()

            val isSuccess = eventSender.sendRecord(eventRecord)

            if (!isSuccess) {
                logger.warn(
                    TAG,
                    "Unable to publish data to ingestion service. Putting it in the dirtyMeetingEvent"
                )

                dirtyEventDao.insertDirtyMeetingEventItems(
                    toDirtyMeetingEventItems(
                        eventItems.map { it.data }.toList()
                    )
                )
            }
            eventDao.deleteMeetingEventsByIds(idsToRemove)
        } catch (exception: Exception) {
            logger.error(
                TAG,
                "Unable to process event ${exception.localizedMessage}"
            )
        }
    }

    private fun toDirtyMeetingEventItems(events: List<SDKEvent>): List<DirtyMeetingEventItem> {
        val currentTime = Calendar.getInstance().timeInMillis
        return events.map {
            DirtyMeetingEventItem(
                data = it,
                ttl = currentTime + TWO_DAYS_IN_MILLISECONDS
            )
        }.toList()
    }

    private fun shouldProcessImmediately(
        event: SDKEvent
    ): Boolean {
        val eventName = event.name
        val eventAttributes = event.eventAttributes

        return (eventName == EventName.meetingFailed.name &&
                eventAttributes[EventAttributeName.meetingStatus.name] == MeetingSessionStatusCode.AudioAuthenticationRejected ||
                eventAttributes[EventAttributeName.meetingStatus.name] == MeetingSessionStatusCode.AudioInternalServerError ||
                eventAttributes[EventAttributeName.meetingStatus.name] == MeetingSessionStatusCode.AudioServiceUnavailable ||
                eventAttributes[EventAttributeName.meetingStatus.name] == MeetingSessionStatusCode.AudioDisconnected)
    }

    // TODO: fix the logic for process dirty events
    // - validEvents can be empty because of that batch of dirty events are all invalid,
    // but there are still dirty events in db
    // - if exception thrown for processing one event, all other events won't be processed
    private fun processDirtyEvents() {
        eventScope.launch {
            try {
                val validEvents = getValidDirtyEvents().toMutableList()
                // Send valid events
                var isSentSuccessful = true
                // There is an known issue with .isNotEmpty() in Mockk in 1.10.0,
                // it has been fixed in new version, however, SDK unit tests fails with new version
                // Will use dirtyEvents.size instead for now
                while (validEvents.size > 0 && isSentSuccessful) {
                    val ingestionRecord =
                        IngestionEventConverter.fromDirtyMeetingEventItems(validEvents, ingestionConfiguration)
                    isSentSuccessful = eventSender.sendRecord(ingestionRecord)

                    // Find the ids that will be removed based on success status
                    if (isSentSuccessful) {
                        dirtyEventDao.deleteDirtyEventsByIds(validEvents.map { it.id })
                    }
                    validEvents.clear()
                    validEvents.addAll(getValidDirtyEvents())
                }
            } catch (exception: Exception) {
                logger.error(TAG, "Unable to process dirty events $exception")
            }
        }
    }

    private fun getValidDirtyEvents(): List<DirtyMeetingEventItem> {
        // Get next batch of dirty events
        val dirtyEvents =
            dirtyEventDao.listDirtyMeetingEventItems(ingestionConfiguration.flushSize)
        val validEvents = mutableListOf<DirtyMeetingEventItem>()
        val invalidEventIds = mutableListOf<String>()

        // Filter valid events
        val currentTime = Calendar.getInstance().timeInMillis
        for (event in dirtyEvents) {
            if (event.ttl > currentTime &&
                event.data.eventAttributes.isNotEmpty() &&
                event.data.eventAttributes[EventAttributeName.timestampMs.name] != null) {
                validEvents.add(event)
            } else {
                invalidEventIds.add(event.id)
            }
        }
        // Delete invalid / expired events
        dirtyEventDao.deleteDirtyEventsByIds(invalidEventIds)

        return validEvents
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import android.content.Context
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.EventTypeConverters
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.DirtyEventSQLiteDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.EventSQLiteDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.SQLiteDatabaseManager
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultMeetingEventReporterFactory(
    private val context: Context,
    private val ingestionConfiguration: IngestionConfiguration,
    private val logger: Logger
) : EventReporterFactory {
    override fun createEventReporter(): EventReporter? {
        if (ingestionConfiguration.disabled) {
            return null
        }
        val eventSender = DefaultEventSender(
            ingestionConfiguration,
            logger
        )
        val sqliteManager =
            SQLiteDatabaseManager(
                context,
                logger
            )

        val eventTypeConverter = EventTypeConverters(logger)

        val eventDao =
            EventSQLiteDao(
                sqliteManager,
                logger,
                eventTypeConverter
            )

        val dirtyEventDao =
            DirtyEventSQLiteDao(
                sqliteManager,
                logger,
                eventTypeConverter
            )

        val eventBuffer = DefaultMeetingEventBuffer(
            ingestionConfiguration,
            eventDao,
            dirtyEventDao,
            eventSender,
            logger
        )
        return DefaultEventReporter(
            ingestionConfiguration,
            eventBuffer,
            logger
        )
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent

/**
 * [EventBuffer] defines a buffer which will consume the [SDKEvent] internally.
 */
interface EventBuffer {
    /**
     * Add a meeting event to the buffer.
     *
     * @param item: [SDKEvent] - meeting event
     */
    fun add(item: SDKEvent)

    /**
     * Consume the data.
     */
    fun process()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

/**
 * [EventClientType] defines the type of event client configuration that will be
 * sent to the server.
 */
enum class EventClientType {
    /**
     * Meeting related type
     */
    Meet
}

/**
 * [EventClientConfiguration] defines core properties needed for every event client configuration.
 */
interface EventClientConfiguration {
    /**
     * type: [EventClientType] - type of [EventClientConfiguration]
     */
    val type: EventClientType

    /**
     * eventClientJoinToken: [String] - authentication token needed for ingestion url
     */
    val eventClientJoinToken: String

    /**
     * tag: [String] - tagging the source of the events, which will be translated to Type for Ingestion Event
     */
    val tag: String

    /**
     * metadataAttributes: [Map<String, Any>] - the attributes that will be sent to Ingestion Service as metadata
     */
    val metadataAttributes: Map<String, Any>
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent

/**
 * [EventReporter] is class that process meeting event that is created in [EventAnalyticsController].
 */
interface EventReporter {
    /**
     * Report the meeting event
     *
     * @param event: [SDKEvent] - Event that has name and attributes associated.
     */
    fun report(event: SDKEvent)

    /**
     * Start [EventReporter] and process data
     */
    fun start()

    /**
     * Stop [EventReporter] and processing data
     */
    fun stop()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

/**
 * [EventReporterFactory] facilitates creating [EventReporter]
 */
interface EventReporterFactory {
    /**
     * Create [EventReporter] and return null if no-op is needed on event reporting.
     *
     * @return [EventReporter] - event reporter created.
     */
    fun createEventReporter(): EventReporter?
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

/**
 * [EventSender] is responsible for sending [IngestionRecord].
 */
interface EventSender {
    /**
     * Send ingestion record.
     *
     * @param record: [IngestionRecord] - record to send
     * @return whether sending was successful or not.
     */
    suspend fun sendRecord(record: IngestionRecord): Boolean
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

const val FLUSH_SIZE_LIMIT_MAXIMUM = 100
const val FLUSH_INTERVAL_LIMIT_MINIMUM: Long = 100
const val RETRY_COUNT_LIMIT_MAXIMUM = 5
/**
 * [IngestionConfiguration] defines the configuration that can customize [DefaultEventReporter].
 *
 * @property clientConfiguration: [EventClientConfiguration] - configuration needed for metadata.
 * @property ingestionUrl: [String] - ingestion server url.
 * @property disabled: [Boolean] - whether ingestion is enabled or disabled. defaults [true].
 * @property flushSize: [Int] - number of payloads to send as a batch. <= 100 and > 0. defaults 20.
 * @property flushIntervalMs: [Long] - duration to wait, pull, and send the data. >= 100. defaults 5000.
 * @property retryCountLimit: [Int] - retry count limit. 0 >= and <= 5. defaults 2.
 */
data class IngestionConfiguration @JvmOverloads constructor(
    val clientConfiguration: EventClientConfiguration,
    var ingestionUrl: String,
    val disabled: Boolean = true,
    var flushSize: Int = 20,
    var flushIntervalMs: Long = 5000,
    var retryCountLimit: Int = 2
) {

    init {
        // Force the max size to 100 as maximum
        // Max payload is 256 kb.
        // Assumption is that each event is max 2kb so we could send ~100 in batch
        // Metadata is about 300~400 bytes so worst case 200 + 300 + extra = 1kb
        // Assumes the worst case 1kb * 2 = 2 kb
        flushSize = flushSize.coerceAtLeast(1).coerceAtMost(FLUSH_SIZE_LIMIT_MAXIMUM)
        // Force the interval to 100 ms minimum
        flushIntervalMs = flushIntervalMs.coerceAtLeast(FLUSH_INTERVAL_LIMIT_MINIMUM)
        // Force retry count to at most 5
        retryCountLimit = retryCountLimit.coerceAtLeast(0).coerceAtMost(RETRY_COUNT_LIMIT_MAXIMUM)
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.google.gson.annotations.SerializedName

typealias IngestionPayload = Map<String, Any>

/**
 * [IngestionEvent] defines the event format ingestion server will accept
 *
 * @property type: [EventClientType] - type of Event.
 * @property payloads: [List<Map<String, Any>] - list of map that contains details of event
 * @property version: [Int] - version of this event. If the format changes, it will have different version.
 */
data class IngestionEvent(
    val type: String,
    val metadata: IngestionMetadata,
    val payloads: List<IngestionPayload>,
    @SerializedName("v")
    val version: Int = 1
)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

/**
 * A record that contains batch of [IngestionEvent] to send.
 * This contains metadata that is shared among events.
 * @property metadata
 * @property events
 *
 * This will have format of following
 * Payload will look like
 * {
 *   "metadata": {
 *        "deviceName": "samsung SM-G960U",
 *        "deviceManufacturer": "samsung",
 *        "deviceModel": "SM-G960U",
 *        "mediaSdkVersion": "0.11.2",
 *        "osName": "Android",
 *        "meetingId": "eb783287-94aa-43c4-9a73-9ead14e697a5",
 *        "attendeeId": "c2435dfb-0c70-4152-be18-efc8773b8b2b"
 *        "osVersion": "10",
 *        "sdkName": "amazon-chime-sdk-android",
 *        "sdkVersion": "0.11.4"
 *    }
 *  "events": [
 *    {
 *      "type": "Meet",
 *      "v": 1,
 *      "payloads": [
 *        {
 *          "ts": 78645356720,
 *          "name": "meetingStartSucceeded",
 *          "maxVideoTileCount": 0,
 *          "poorConnectionCount": 0,
 *          "retryCount": 0,
 *          "signalingOpenDurationMs": 666
 *        },
 *        {
 *          "ts": 78645356797,
 *          "name": "meetingEnded",
 *          "maxVideoTileCount": 0,
 *          "poorConnectionCount": 0,
 *          "retryCount": 0,
 *          "signalingOpenDurationMs": 444
 *        }
 *      ]
 *    }
 *  ]
 * }
 */
typealias IngestionMetadata = Map<String, Any>

data class IngestionRecord(val metadata: IngestionMetadata, val events: List<IngestionEvent>)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName

/**
 * [MeetingEventClientConfiguration] defines one type of [EventClientConfiguration]
 * that is needed for [DefaultEventReporter]
 *
 * @property eventClientJoinToken: [String] - an authorization token to send
 * @property meetingId: [String] - meeting id
 * @property attendeeId: [String] - attendee id
 */
data class MeetingEventClientConfiguration(
    override val eventClientJoinToken: String,
    val meetingId: String,
    val attendeeId: String
) : EventClientConfiguration {
    override val type: EventClientType = EventClientType.Meet

    override val tag: String = "Meet"
    override val metadataAttributes: Map<String, Any> =
        mapOf(
            EventAttributeName.meetingId.name to meetingId,
            EventAttributeName.attendeeId.name to attendeeId
        )
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.ingestion

/**
 * [NoopEventReporterFactory] returns null [EventReporter]
 */
class NoopEventReporterFactory : EventReporterFactory {
    override fun createEventReporter(): EventReporter? {
        return null
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal

/**
 * [AttendeeStatus] describes status of Attendee - Join, Leave, Drop
 */
enum class AttendeeStatus(val value: Int) {
    /**
     * The attendee joined
     */
    Joined(1),

    /**
     * The attendee left
     */
    Left(2),

    /**
     * The attendee dropped due to network
     */
    Dropped(3);

    companion object {
        fun from(intValue: Int): AttendeeStatus? = values().find { it.value == intValue }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal

/**
 * [SessionStateControllerAction] is a state-changing action to perform.
 */
enum class SessionStateControllerAction(val value: Int) {
    Unknown(-1),
    Init(0),
    Connecting(1),
    FinishConnecting(2),
    Updating(3),
    FinishUpdating(4),
    Reconnecting(5),
    Disconnecting(6),
    FinishDisconnecting(7),
    Fail(8);
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import com.amazonaws.services.chime.sdk.meetings.audiovideo.PrimaryMeetingPromotionObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioRecordingPresetOverride
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioStreamType
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials

/**
 * [AudioClientController]'s responsibility is to handle AudioClient API calls such as starting
 * and stopping audio session
 *
 * Interface was created in response to difficulty in refactoring [AudioClientController] into
 * [DefaultAudioClientController] and [DefaultAudioClientObserver] without breaking the rest of code
 */
interface AudioClientController {
    fun getRoute(): Int
    fun setRoute(route: Int): Boolean
    fun start(
        audioFallbackUrl: String,
        audioHostUrl: String,
        meetingId: String,
        attendeeId: String,
        joinToken: String,
        audioMode: AudioMode,
        audioStreamType: AudioStreamType,
        audioRecordingPresetOverride: AudioRecordingPresetOverride
    )

    fun stop()
    fun setMute(isMuted: Boolean): Boolean
    fun setVoiceFocusEnabled(enabled: Boolean): Boolean
    fun isVoiceFocusEnabled(): Boolean
    fun promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver)
    fun demoteFromPrimaryMeeting()
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import android.content.Context
import com.xodee.client.audio.audioclient.AudioClient

class AudioClientFactory private constructor(
    context: Context,
    audioClientObserver: AudioClientObserver
) {
    private val audioClient: AudioClient

    init {
        System.loadLibrary("c++_shared")
        System.loadLibrary("amazon_chime_media_client")
        audioClient = AudioClient(
            context.assets,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            audioClientObserver,
            0
        )
    }

    companion object {
        fun getAudioClient(
            context: Context,
            audioClientObserver: AudioClientObserver
        ): AudioClient {
            return AudioClientFactory(
                context,
                audioClientObserver
            ).audioClient
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.PrimaryMeetingPromotionObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.TranscriptEventObserver
import com.xodee.client.audio.audioclient.AudioClientLogListener
import com.xodee.client.audio.audioclient.AudioClientMetricsListener
import com.xodee.client.audio.audioclient.AudioClientPresenceListener
import com.xodee.client.audio.audioclient.AudioClientPrimaryMeetingEventListener
import com.xodee.client.audio.audioclient.AudioClientSignalStrengthChangeListener
import com.xodee.client.audio.audioclient.AudioClientStateChangeListener
import com.xodee.client.audio.audioclient.AudioClientTranscriptEventsListener
import com.xodee.client.audio.audioclient.AudioClientVolumeStateChangeListener

/**
 * [AudioClientObserver]'s responsibility is to handle AudioClient callbacks and maintain all
 * the observers ([AudioVideoObserver], [RealtimeObserver]) that need to be notified
 */
interface AudioClientObserver : AudioClientStateChangeListener,
    AudioClientVolumeStateChangeListener,
    AudioClientSignalStrengthChangeListener, AudioClientLogListener,
    AudioClientMetricsListener, AudioClientPresenceListener,
    AudioClientTranscriptEventsListener, AudioClientPrimaryMeetingEventListener {

    fun subscribeToAudioClientStateChange(observer: AudioVideoObserver)
    fun unsubscribeFromAudioClientStateChange(observer: AudioVideoObserver)
    fun notifyAudioClientObserver(observerFunction: (observer: AudioVideoObserver) -> Unit)
    fun subscribeToRealTimeEvents(observer: RealtimeObserver)
    fun unsubscribeFromRealTimeEvents(observer: RealtimeObserver)
    fun subscribeToTranscriptEvent(observer: TranscriptEventObserver)
    fun unsubscribeFromTranscriptEvent(observer: TranscriptEventObserver)
    // This only supports one observer at a time
    var primaryMeetingPromotionObserver: PrimaryMeetingPromotionObserver?
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

/**
 * The current state of the Audio Client. Used by [AudioClientController]
 * to determine if actions are allowed based on current state.
 */
enum class AudioClientState(val value: Int) {
    INITIALIZED(0),
    STARTED(1),
    STOPPED(2),
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import android.content.Context
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.PrimaryMeetingPromotionObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioRecordingPresetOverride
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioStreamType
import com.amazonaws.services.chime.sdk.meetings.internal.utils.AppInfoUtil
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.audio.audioclient.AudioClient
import com.xodee.client.audio.audioclient.AudioClient.AudioModeInternal
import com.xodee.client.audio.audioclient.AudioClientSessionConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class DefaultAudioClientController(
    private val context: Context,
    private val logger: Logger,
    private val audioClientObserver: AudioClientObserver,
    private val audioClient: AudioClient,
    private val meetingStatsCollector: MeetingStatsCollector,
    private val eventAnalyticsController: EventAnalyticsController
) : AudioClientController {
    private val TAG = "DefaultAudioClientController"
    private val DEFAULT_PORT = 0 // In case the URL does not have port
    private val AUDIO_PORT_OFFSET = 200 // Offset by 200 so that subtraction results in 0
    private val DEFAULT_PRESENTER = true
    private val AUDIO_CLIENT_RESULT_SUCCESS = AudioClient.AUDIO_CLIENT_OK

    private var muteMicAndSpeaker = false
    private val uiScope = CoroutineScope(Dispatchers.Main)
    private val audioManager: AudioManager =
        context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private var audioModePreCall: Int = audioManager.mode
    private var speakerphoneStatePreCall: Boolean = audioManager.isSpeakerphoneOn

    companion object {
        var audioClientState = AudioClientState.INITIALIZED
    }

    private fun setUpAudioConfiguration(audioMode: AudioMode) {
        // There seems to be no call that gives us the native input sample rate, so we just use the output rate
        val nativeSR = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_SYSTEM)

        // The HARDWARE_SAMPLERATE is currently used to construct the proper buffer sizes
        audioClient.sendMessage(AudioClient.MESS_SET_HARDWARE_SAMPLE_RATE, nativeSR)

        // This IO_SAMPLE_RATE is used to create OpenSLES:
        val samplingRateConfig = when (audioMode) {
            AudioMode.Mono16K -> 16000
            AudioMode.Mono48K -> 48000
            AudioMode.Stereo48K -> 48000
            AudioMode.NoDevice -> 16000
        }
        audioClient.sendMessage(
            AudioClient.MESS_SET_IO_SAMPLE_RATE,
            samplingRateConfig
        )

        // Result is in bytes, so we divide by 2 (16-bit samples)
        val outputChannelConfig = when (audioMode) {
            AudioMode.Mono16K -> AudioFormat.CHANNEL_OUT_MONO
            AudioMode.Mono48K -> AudioFormat.CHANNEL_OUT_MONO
            AudioMode.Stereo48K -> AudioFormat.CHANNEL_OUT_STEREO
            AudioMode.NoDevice -> AudioFormat.CHANNEL_OUT_MONO
        }
        val spkMinBufSizeInSamples = AudioTrack.getMinBufferSize(
            nativeSR,
            outputChannelConfig,
            AudioFormat.ENCODING_PCM_16BIT
        ) / 2

        val micMinBufSizeInSamples = AudioRecord.getMinBufferSize(
            nativeSR,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT
        ) / 2

        logger.info(
            TAG,
            "spkMinBufSizeInSamples $spkMinBufSizeInSamples micMinBufSizeInSamples $micMinBufSizeInSamples"
        )
        audioClient.sendMessage(AudioClient.MESS_SET_MIC_FRAMES_PER_BUFFER, micMinBufSizeInSamples)
        audioClient.sendMessage(AudioClient.MESS_SET_SPK_FRAMES_PER_BUFFER, spkMinBufSizeInSamples)
        audioClient.sendMessage(
            AudioClient.MESS_SET_SPEAKERPHONE_MIC,
            AudioClient.OPENSL_MIC_DEFAULT
        )
        audioClient.sendMessage(AudioClient.MESS_SET_CVP_MODULE_FLAG, AudioClient.CVP_MODULE_NONE)
        audioClient.sendMessage(AudioClient.MESS_SET_CVP_PREF_FLAG, AudioClient.CVP_PREF_NONE)
    }

    override fun getRoute(): Int {
        return audioClient.route
    }

    override fun setRoute(route: Int): Boolean {
        if (getRoute() == route) return true
        logger.info(TAG, "Setting route to $route")

        return audioClient.setRoute(route) == AUDIO_CLIENT_RESULT_SUCCESS
    }

    private fun getDefaultRecordingPreset(): AudioClient.AudioRecordingPreset {
        val recordingPreset = AudioClient.AudioRecordingPreset.VOICE_COMMUNICATION
        logger.info(TAG, "No AudioRecordingPreseOverride provided, using recording preset $recordingPreset")
        return recordingPreset
    }

    override fun start(
        audioFallbackUrl: String,
        audioHostUrl: String,
        meetingId: String,
        attendeeId: String,
        joinToken: String,
        audioMode: AudioMode,
        audioStreamType: AudioStreamType,
        audioRecordingPresetOverride: AudioRecordingPresetOverride
    ) {
        // Validate audio client state
        if (audioClientState != AudioClientState.INITIALIZED &&
            audioClientState != AudioClientState.STOPPED
        ) {
            logger.warn(
                TAG,
                "Current audio client state $audioClientState is invalid to start audio, ignoring"
            )
            return
        }

        val audioUrlParts: List<String> =
            audioHostUrl.split(":".toRegex()).dropLastWhile { it.isEmpty() }

        val (host: String, portStr: String) = if (audioUrlParts.size == 2) audioUrlParts else listOf(
            audioUrlParts[0],
            "$AUDIO_PORT_OFFSET"
        )

        // We subtract 200 here since audio client will add an offset of 200 for the DTLS port
        val port = try {
            Integer.parseInt(portStr) - AUDIO_PORT_OFFSET
        } catch (exception: Exception) {
            logger.warn(
                TAG,
                "Error parsing int. Using default value. Exception: ${exception.message}"
            )
            DEFAULT_PORT
        }
        setUpAudioConfiguration(audioMode)
        meetingStatsCollector.updateMeetingStartConnectingTimeMs()
        eventAnalyticsController.publishEvent(EventName.meetingStartRequested)
        audioClientObserver.notifyAudioClientObserver { observer ->
            observer.onAudioSessionStartedConnecting(
                false
            )
        }
        audioManager.mode = AudioManager.MODE_IN_COMMUNICATION

        val appInfo = AppInfoUtil.initializeAudioClientAppInfo(context)

        uiScope.launch {
            val audioModeInternal = when (audioMode) {
                AudioMode.Mono16K -> AudioModeInternal.MONO_16K
                AudioMode.Mono48K -> AudioModeInternal.MONO_48K
                AudioMode.Stereo48K -> AudioModeInternal.STEREO_48K
                AudioMode.NoDevice -> AudioModeInternal.NO_DEVICE
            }

            var audioStreamTypeInternal = when (audioStreamType) {
                AudioStreamType.VoiceCall -> AudioClient.AudioStreamType.VOICE_CALL
                AudioStreamType.Music -> AudioClient.AudioStreamType.MUSIC
            }

            var audioRecordingPresetInternal = when (audioRecordingPresetOverride) {
                AudioRecordingPresetOverride.None -> getDefaultRecordingPreset()
                AudioRecordingPresetOverride.Generic -> AudioClient.AudioRecordingPreset.GENERIC
                AudioRecordingPresetOverride.Camcorder -> AudioClient.AudioRecordingPreset.CAMCORDER
                AudioRecordingPresetOverride.VoiceRecognition -> AudioClient.AudioRecordingPreset.VOICE_RECOGNITION
                AudioRecordingPresetOverride.VoiceCommunication -> AudioClient.AudioRecordingPreset.VOICE_COMMUNICATION
            }

            var config = AudioClientSessionConfig.Builder(
                host,
                port,
                joinToken,
                meetingId,
                attendeeId,
                audioFallbackUrl,
                appInfo,
                audioModeInternal,
                audioStreamTypeInternal,
                audioRecordingPresetInternal
            ).withTransportMode(AudioClient.XTL_DEFAULT_TRANSPORT)
                .withMicMute(muteMicAndSpeaker)
                .withSpkMute(muteMicAndSpeaker)
                .withPresenter(DEFAULT_PRESENTER)
                .withProxyConfig(null)
                .build()

            val res = audioClient.startSession(config)

            if (res != AUDIO_CLIENT_RESULT_SUCCESS) {
                logger.error(TAG, "Failed to start audio session. Response code: $res")

                eventAnalyticsController.publishEvent(
                    EventName.meetingStartFailed
                )
            } else {
                logger.info(TAG, "Started audio session.")
                audioClientState = AudioClientState.STARTED
            }
        }
    }

    override fun stop() {
        if (audioClientState != AudioClientState.STARTED) {
            logger.error(
                TAG,
                "Current audio client state $audioClientState is invalid to stop audio, ignoring"
            )
            return
        }

        GlobalScope.launch {
            val res = audioClient.stopSession()

            if (res != AUDIO_CLIENT_RESULT_SUCCESS) {
                logger.error(TAG, "Failed to stop audio session. Response code: $res")
            } else {
                logger.info(TAG, "Stopped audio session.")
                audioClientState = AudioClientState.STOPPED
                resetAudioManager()
                notifyStop()
                audioClientObserver.notifyAudioClientObserver { observer ->
                    observer.onAudioSessionStopped(
                        MeetingSessionStatus(MeetingSessionStatusCode.OK)
                    )
                }
            }
        }
    }

    private fun notifyStop() {
        eventAnalyticsController.publishEvent(
            EventName.meetingEnded,
            mutableMapOf(EventAttributeName.meetingStatus to MeetingSessionStatusCode.OK)
        )
        meetingStatsCollector.resetMeetingStats()
    }

    private fun resetAudioManager() {
        audioManager.apply {
            isBluetoothScoOn = false
            stopBluetoothSco()
        }
        audioManager.mode = audioModePreCall
        audioManager.isSpeakerphoneOn = speakerphoneStatePreCall
    }

    override fun setMute(isMuted: Boolean): Boolean {
        return audioClientState == AudioClientState.STARTED && AudioClient.AUDIO_CLIENT_OK == audioClient.setMicMute(
            isMuted
        )
    }

    override fun setVoiceFocusEnabled(enabled: Boolean): Boolean {
        if (audioClientState == AudioClientState.STARTED) {
            return AudioClient.AUDIO_CLIENT_OK == audioClient.setVoiceFocusNoiseSuppression(enabled)
        } else {
            logger.error(
                TAG,
                "Failed to set VoiceFocus to $enabled; audio client state is $audioClientState"
            )
            return false
        }
    }

    override fun isVoiceFocusEnabled(): Boolean {
        if (audioClientState == AudioClientState.STARTED) {
            return audioClient.getVoiceFocusNoiseSuppression()
        } else {
            logger.error(
                TAG,
                "Failed to get VoiceFocus enabled state; audio client state is $audioClientState"
            )
            return false
        }
    }

    override fun promoteToPrimaryMeeting(
        credentials: MeetingSessionCredentials,
        observer: PrimaryMeetingPromotionObserver
    ) {
        if (audioClientState != AudioClientState.STARTED) {
            observer.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable))
            return
        }
        audioClientObserver.primaryMeetingPromotionObserver = observer
        audioClient.promoteToPrimaryMeeting(credentials.attendeeId, credentials.externalUserId, credentials.joinToken)
    }

    override fun demoteFromPrimaryMeeting() {
        if (audioClientState != AudioClientState.STARTED) {
            return
        }
        audioClient.demoteFromPrimaryMeeting()
    }
}