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

package com.amazonaws.services.chime.sdkdemo.activity

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.backgroundblur.BackgroundBlurConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.backgroundblur.BackgroundBlurVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.backgroundreplacement.BackgroundReplacementVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.CameraCaptureSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.DefaultCameraCaptureSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.DefaultSurfaceTextureCaptureSourceFactory
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdk.meetings.session.CreateAttendeeResponse
import com.amazonaws.services.chime.sdk.meetings.session.CreateMeetingResponse
import com.amazonaws.services.chime.sdk.meetings.session.DefaultMeetingSession
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger
import com.amazonaws.services.chime.sdk.meetings.utils.logger.LogLevel
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.JoinMeetingResponse
import com.amazonaws.services.chime.sdkdemo.device.ScreenShareManager
import com.amazonaws.services.chime.sdkdemo.fragment.DeviceManagementFragment
import com.amazonaws.services.chime.sdkdemo.fragment.MeetingFragment
import com.amazonaws.services.chime.sdkdemo.model.MeetingSessionModel
import com.amazonaws.services.chime.sdkdemo.utils.CpuVideoProcessor
import com.amazonaws.services.chime.sdkdemo.utils.GpuVideoProcessor
import com.google.gson.Gson

class MeetingActivity : AppCompatActivity(),
    DeviceManagementFragment.DeviceManagementEventListener,
    MeetingFragment.RosterViewEventListener {

    private val logger = ConsoleLogger(LogLevel.DEBUG)
    private val gson = Gson()
    private val meetingSessionModel: MeetingSessionModel by lazy { ViewModelProvider(this)[MeetingSessionModel::class.java] }

    private lateinit var meetingId: String
    private var primaryExternalMeetingId: String? = null
    private lateinit var name: String
    private lateinit var audioVideoConfig: AudioVideoConfiguration
    private lateinit var meetingEndpointUrl: String

    private var cachedDevice: MediaDevice? = null

    private val TAG = "InMeetingActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meeting)
        meetingId = intent.extras?.getString(HomeActivity.MEETING_ID_KEY) as String
        name = intent.extras?.getString(HomeActivity.NAME_KEY) as String
        val audioMode = intent.extras?.getInt(HomeActivity.AUDIO_MODE_KEY)?.let { intValue ->
            AudioMode.from(intValue, defaultAudioMode = AudioMode.Stereo48K)
        } ?: AudioMode.Stereo48K
        audioVideoConfig = AudioVideoConfiguration(audioMode = audioMode)
        meetingEndpointUrl = intent.extras?.getString(HomeActivity.MEETING_ENDPOINT_KEY) as String

        if (savedInstanceState == null) {
            val meetingResponseJson =
                intent.extras?.getString(HomeActivity.MEETING_RESPONSE_KEY) as String
            val sessionConfig =
                createSessionConfigurationAndExtractPrimaryMeetingInformation(meetingResponseJson)
            val meetingSession = sessionConfig?.let {
                logger.info(TAG, "Creating meeting session for meeting Id: $meetingId")

                DefaultMeetingSession(
                    it,
                    logger,
                    applicationContext,
                    // Note if the following isn't provided app will (as expected) crash if we use custom video source
                    // since an EglCoreFactory will be internal created and will be using a different shared EGLContext.
                    // However the internal default capture would work fine, since it is initialized using
                    // that internally created default EglCoreFactory, and can be smoke tested by removing this
                    // argument and toggling use of custom video source before starting video
                    meetingSessionModel.eglCoreFactory
                )
            }

            if (meetingSession == null) {
                Toast.makeText(
                    applicationContext,
                    getString(R.string.user_notification_meeting_start_error),
                    Toast.LENGTH_LONG
                ).show()
                finish()
            } else {
                meetingSessionModel.meetingSession = meetingSession
                meetingSessionModel.primaryExternalMeetingId = primaryExternalMeetingId
            }

            val surfaceTextureCaptureSourceFactory = DefaultSurfaceTextureCaptureSourceFactory(
                logger,
                meetingSessionModel.eglCoreFactory
            )
            meetingSessionModel.cameraCaptureSource = DefaultCameraCaptureSource(
                applicationContext,
                logger,
                surfaceTextureCaptureSourceFactory
            ).apply {
                eventAnalyticsController = meetingSession?.eventAnalyticsController
            }
            meetingSessionModel.cpuVideoProcessor =
                CpuVideoProcessor(logger, meetingSessionModel.eglCoreFactory)
            meetingSessionModel.gpuVideoProcessor =
                GpuVideoProcessor(logger, meetingSessionModel.eglCoreFactory)
            meetingSessionModel.backgroundBlurVideoFrameProcessor =
                BackgroundBlurVideoFrameProcessor(
                    logger,
                    meetingSessionModel.eglCoreFactory,
                    applicationContext,
                    BackgroundBlurConfiguration()
                )
            meetingSessionModel.backgroundReplacementVideoFrameProcessor =
                BackgroundReplacementVideoFrameProcessor(
                    logger,
                    meetingSessionModel.eglCoreFactory,
                    applicationContext,
                    null
                )

            val deviceManagementFragment =
                DeviceManagementFragment.newInstance(meetingId, name, audioVideoConfig)
            supportFragmentManager
                .beginTransaction()
                .add(R.id.root_layout, deviceManagementFragment, "deviceManagement")
                .commit()
        }
    }

    override fun onJoinMeetingClicked() {
        val rosterViewFragment =
            MeetingFragment.newInstance(meetingId, audioVideoConfig, meetingEndpointUrl)
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.root_layout, rosterViewFragment, "rosterViewFragment")
            .commit()
    }

    override fun onCachedDeviceSelected(mediaDevice: MediaDevice) {
        cachedDevice = mediaDevice
    }

    override fun onLeaveMeeting() {
        onBackPressed()
    }

    override fun onDestroy() {
        if (isFinishing) {
            cleanup()
        }
        super.onDestroy()
    }

    private fun cleanup() {
        meetingSessionModel.audioVideo.stopLocalVideo()
        meetingSessionModel.audioVideo.stopRemoteVideo()
        meetingSessionModel.audioVideo.stopContentShare()
        meetingSessionModel.audioVideo.stop()
        meetingSessionModel.cameraCaptureSource.stop()
        meetingSessionModel.gpuVideoProcessor.release()
        meetingSessionModel.cpuVideoProcessor.release()
        meetingSessionModel.backgroundBlurVideoFrameProcessor.release()
        meetingSessionModel.backgroundReplacementVideoFrameProcessor.release()
        meetingSessionModel.screenShareManager?.stop()
        meetingSessionModel.screenShareManager?.release()
    }

    fun getAudioVideo(): AudioVideoFacade = meetingSessionModel.audioVideo

    fun getMeetingSessionConfiguration(): MeetingSessionConfiguration =
        meetingSessionModel.configuration

    fun getMeetingSessionCredentials(): MeetingSessionCredentials = meetingSessionModel.credentials

    fun getPrimaryExternalMeetingId(): String? = meetingSessionModel.primaryExternalMeetingId

    fun getCachedDevice(): MediaDevice? = cachedDevice
    fun resetCachedDevice() {
        cachedDevice = null
    }

    fun getEglCoreFactory(): EglCoreFactory = meetingSessionModel.eglCoreFactory

    fun getCameraCaptureSource(): CameraCaptureSource = meetingSessionModel.cameraCaptureSource

    fun getGpuVideoProcessor(): GpuVideoProcessor = meetingSessionModel.gpuVideoProcessor

    fun getCpuVideoProcessor(): CpuVideoProcessor = meetingSessionModel.cpuVideoProcessor

    fun getBackgroundBlurVideoFrameProcessor(): BackgroundBlurVideoFrameProcessor =
        meetingSessionModel.backgroundBlurVideoFrameProcessor

    fun getBackgroundReplacementVideoFrameProcessor(): BackgroundReplacementVideoFrameProcessor =
        meetingSessionModel.backgroundReplacementVideoFrameProcessor

    fun getScreenShareManager(): ScreenShareManager? = meetingSessionModel.screenShareManager

    fun setScreenShareManager(screenShareManager: ScreenShareManager?) {
        meetingSessionModel.screenShareManager = screenShareManager
    }

    private fun urlRewriter(url: String): String {
        // You can change urls by url.replace("example.com", "my.example.com")
        return url
    }

    private fun createSessionConfigurationAndExtractPrimaryMeetingInformation(response: String?): MeetingSessionConfiguration? {
        if (response.isNullOrBlank()) return null
        return try {
            val joinMeetingResponse = gson.fromJson(response, JoinMeetingResponse::class.java)
            primaryExternalMeetingId = joinMeetingResponse.joinInfo.primaryExternalMeetingId
            MeetingSessionConfiguration(
                CreateMeetingResponse(joinMeetingResponse.joinInfo.meetingResponse.meeting),
                CreateAttendeeResponse(joinMeetingResponse.joinInfo.attendeeResponse.attendee),
                ::urlRewriter
            )
        } catch (exception: Exception) {
            logger.error(
                TAG,
                "Error creating session configuration: ${exception.localizedMessage}"
            )
            null
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.activity

import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.Handler
import android.view.Window
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import com.amazonaws.services.chime.sdkdemo.R

class SplashActivity : AppCompatActivity() {

    private val SPLASH_TIME_OUT_MILLISECOND: Long = 3000

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
        window.requestFeature(Window.FEATURE_NO_TITLE)
        window.setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
        )
        setContentView(R.layout.activity_splash)

        Handler().postDelayed({
            startActivity(Intent(this@SplashActivity, HomeActivity::class.java))
            finish()
        }, SPLASH_TIME_OUT_MILLISECOND)
    }
}


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

package com.amazonaws.services.chime.sdkdemo.activity

import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
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.ConsoleLogger
import com.amazonaws.services.chime.sdk.meetings.utils.logger.LogLevel
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.SpinnerItem
import com.amazonaws.services.chime.sdkdemo.data.TranscriptionStreamParams
import com.amazonaws.services.chime.sdkdemo.fragment.TranscriptionConfigFragment
import com.amazonaws.services.chime.sdkdemo.utils.encodeURLParam
import com.amazonaws.services.chime.sdkdemo.utils.showToast
import com.google.gson.Gson
import java.net.URL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class TranscriptionConfigActivity : AppCompatActivity(),
    TranscriptionConfigFragment.TranscriptionConfigurationEventListener {

    companion object {
        private const val TRANSCRIBE_CONTENT_IDENTIFICATION_TYPE = "PII"
        private const val TRANSCRIBE_MEDICAL_CONTENT_IDENTIFICATION_TYPE = "PHI"
        private const val TRANSCRIBE_MEDICAL_ENGINE = "transcribe_medical"
        private const val TRANSCRIBE_PARTIAL_RESULTS_STABILIZATION_DEFAULT = "default"
        private const val TRANSCRIBE_PARTIAL_RESULTS_STABILIZATION_HIGH = "high"
    }

    private val logger = ConsoleLogger(LogLevel.INFO)

    private val uiScope = CoroutineScope(Dispatchers.Main)

    private lateinit var meetingId: String

    private val TAG = "TranscriptionConfigActivity"

    private lateinit var meetingEndpointUrl: String

    private val gson = Gson()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_transcription_config)
        meetingId = intent.getStringExtra(HomeActivity.MEETING_ID_KEY) as String
        meetingEndpointUrl = intent.getStringExtra(HomeActivity.MEETING_ENDPOINT_KEY) as String
        if (savedInstanceState == null) {
            val transcriptionConfigFragment = TranscriptionConfigFragment.newInstance(meetingId)
            supportFragmentManager
                .beginTransaction()
                .add(R.id.root_layout, transcriptionConfigFragment, "transcriptionConfig")
                .commit()
        }
    }

    override fun onContextItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> {
                finish()
                return true
            }
        }
        return super.onContextItemSelected(item)
    }

    override fun onStartTranscription(
        engine: SpinnerItem,
        language: SpinnerItem?,
        region: SpinnerItem,
        partialResultsStability: SpinnerItem,
        contentIdentificationType: SpinnerItem?,
        contentRedactionType: SpinnerItem?,
        languageModelName: String?,
        identifyLanguage: Boolean,
        languageOptions: String?,
        preferredLanguage: SpinnerItem?
    ) {
        uiScope.launch {
            val response: String? =
                enableMeetingTranscription(
                    meetingId,
                    engine.spinnerText,
                    language?.spinnerText,
                    region.spinnerText,
                    partialResultsStability.spinnerText,
                    contentIdentificationType?.spinnerText,
                    contentRedactionType?.spinnerText,
                    languageModelName,
                    identifyLanguage,
                    languageOptions,
                    preferredLanguage?.spinnerText
                )

            if (response == null) {
                showToast(getString(R.string.user_notification_transcription_start_error))
            } else {
                showToast(getString(R.string.user_notification_transcription_start_success))
            }
            onBackPressed()
        }
    }

    private suspend fun enableMeetingTranscription(
        meetingId: String?,
        engine: String?,
        languageCode: String?,
        region: String?,
        transcribePartialResultsStabilization: String?,
        transcribeContentIdentification: String?,
        transcribeContentRedaction: String?,
        customLanguageModel: String?,
        identifyLanguage: Boolean?,
        languageOptions: String?,
        preferredLanguage: String?
    ): String? {
        val partialResultsStabilizationEnabled = transcribePartialResultsStabilization != null
        val isTranscribeMedical = engine.equals(TRANSCRIBE_MEDICAL_ENGINE)
        val transcriptionStreamParams = TranscriptionStreamParams(
            contentIdentificationType = transcribeContentIdentification?.let {
                if (isTranscribeMedical) TRANSCRIBE_MEDICAL_CONTENT_IDENTIFICATION_TYPE
                else TRANSCRIBE_CONTENT_IDENTIFICATION_TYPE
            },
            contentRedactionType = transcribeContentRedaction?.let { TRANSCRIBE_CONTENT_IDENTIFICATION_TYPE },
            enablePartialResultsStabilization = partialResultsStabilizationEnabled,
            partialResultsStability = transcribePartialResultsStabilization?.let {
                if (it == TRANSCRIBE_PARTIAL_RESULTS_STABILIZATION_DEFAULT) TRANSCRIBE_PARTIAL_RESULTS_STABILIZATION_HIGH
                else it
            },
            piiEntityTypes = transcribeContentIdentification?.let { identification ->
                if (identification.isEmpty() || isTranscribeMedical) null
                else identification
            } ?: run { if (transcribeContentRedaction.isNullOrEmpty()) null else transcribeContentRedaction },
            languageModelName = customLanguageModel?.ifEmpty { null },
            identifyLanguage = identifyLanguage?.let { if (it) { true } else null },
            languageOptions = languageOptions?.ifEmpty { null },
            preferredLanguage = preferredLanguage?.ifEmpty { null }
        )
        val transcriptionAdditionalParams = gson.toJson(transcriptionStreamParams)
        val languageCodeParams = if (isTranscribeMedical || (identifyLanguage == false)) {
            "&language=${encodeURLParam(languageCode)}"
        } else ""
        val meetingUrl = if (meetingEndpointUrl.endsWith("/")) meetingEndpointUrl else meetingEndpointUrl.plus("/")
        val url = "${meetingUrl}start_transcription?title=${encodeURLParam(meetingId)}" +
                languageCodeParams +
                "&region=${encodeURLParam(region)}" +
                "&engine=${encodeURLParam(engine)}" +
                "&transcriptionStreamParams=${encodeURLParam(transcriptionAdditionalParams)}"
        val response = HttpUtils.post(URL(url), "", DefaultBackOffRetry(), logger)

        return if (response.httpException == null) {
            response.data
        } else {
            logger.error(TAG, "Error sending start transcription request ${response.httpException}")
            null
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.LineBackgroundSpan
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptItemType
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.Caption
import com.amazonaws.services.chime.sdkdemo.utils.inflate
import kotlinx.android.synthetic.main.row_caption.view.captionText
import kotlinx.android.synthetic.main.row_caption.view.speakerName

class CaptionAdapter(
    private val captions: Collection<Caption>
) :
    RecyclerView.Adapter<CaptionHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CaptionHolder {
        val inflatedView = parent.inflate(R.layout.row_caption, false)
        return CaptionHolder(inflatedView)
    }

    override fun getItemCount(): Int {
        return captions.size
    }

    override fun onBindViewHolder(holder: CaptionHolder, position: Int) {
        holder.bindCaption(captions.elementAt(position), position)
    }
}

class CaptionHolder(inflatedView: View) :
    RecyclerView.ViewHolder(inflatedView) {

    companion object {
        private const val UPPER_THRESHOLD = 0.3
        private const val LOWER_THRESHOLD = 0.0
        private const val FILTERED_CAPTION_FIRST_INDEX = "["
    }

    private var view: View = inflatedView

    fun bindCaption(caption: Caption, position: Int) {
        val speakerName = caption.speakerName ?: ""

        val captionTextBackgroundColor = caption.speakerName?.let { R.color.colorWhite } ?: R.color.colorMissingSpeaker
        view.captionText.setBackgroundResource(captionTextBackgroundColor)
        view.speakerName.text = speakerName
        view.captionText.text = caption.content
        val spannable = SpannableString(caption.content)
        caption.entities?.let { contents ->
            // Highlight PII identified and redacted words.
            contents.forEach { word ->
                spannable.setSpan(
                    ForegroundColorSpan(Color.GREEN),
                    caption.content.indexOf(word),
                    caption.content.indexOf(word) + word.length,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                )
            }
        } ?: run { view.captionText.setTextColor(Color.BLACK) }
        caption.items?.let { items ->
            // Underline words with low confidence score.
            items.forEach { item ->
                val word = item.content
                val hasLowConfidence = item.confidence?.let { it > LOWER_THRESHOLD && it < UPPER_THRESHOLD } ?: run { false }
                val isCorrectContentType = !word.startsWith(FILTERED_CAPTION_FIRST_INDEX) && item.type != TranscriptItemType.Punctuation
                if (hasLowConfidence && isCorrectContentType && caption.content.contains(word)) {
                    spannable.setSpan(
                        CustomUnderlineSpan(Color.RED, caption.content.indexOf(word), caption.content.indexOf(word) + word.length),
                        caption.content.indexOf(word),
                        caption.content.indexOf(word) + word.length,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                }
            }
        }
        view.captionText.text = spannable
        view.captionText.contentDescription = "caption-$position"
    }
}

private class CustomUnderlineSpan(underlineColor: Int, underlineStart: Int, underlineEnd: Int) : LineBackgroundSpan {

    var color: Int
    var start: Int
    var end: Int
    var underlinePaint: Paint

    init {
        this.color = underlineColor
        this.start = underlineStart
        this.end = underlineEnd
        underlinePaint = Paint()
        underlinePaint.setColor(color)
        underlinePaint.strokeWidth = 3F
        underlinePaint.style = Paint.Style.FILL_AND_STROKE
    }

    override fun drawBackground(
        canvas: Canvas,
        paint: Paint,
        left: Int,
        right: Int,
        top: Int,
        baseline: Int,
        bottom: Int,
        text: CharSequence,
        start: Int,
        end: Int,
        lnum: Int
    ) {
        if (this.end <= start) return
        if (this.start >= end || this.start < 0) return

        var offsetX = 0
        if (this.start > start) {
            offsetX = paint.measureText(text.subSequence(start, this.start).toString()).toInt()
        }

        val length = paint.measureText(text.subSequence(
            Math.max(start, this.start),
            Math.min(end, this.end)
        ).toString()).toInt()
        canvas.drawLine(offsetX.toFloat(), baseline + 3F, (length + offsetX).toFloat(), baseline + 3F, this.underlinePaint)
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.content.Context
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdkdemo.device.AudioDeviceManager

internal const val AUDIO_RECORDING_CONFIG_API_LEVEL = 24

class DeviceAdapter(
    context: Context,
    resource: Int,
    private val devices: List<MediaDevice>,
    private val audioVideo: AudioVideoFacade,
    private val audioManager: AudioDeviceManager
) :
    ArrayAdapter<MediaDevice>(context, resource, devices) {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = super.getView(position, convertView, parent) as TextView
        view.contentDescription = devices[position].type.name
        if (Build.VERSION.SDK_INT >= AUDIO_RECORDING_CONFIG_API_LEVEL) {
            val currentDevice = audioVideo.getActiveAudioDevice()
            view.text =
                if (currentDevice == devices[position]) "${devices[position]} âœ“" else devices[position].toString()
        } else {
            val currentDevice = audioManager.activeAudioDevice
            view.text =
                if (currentDevice == devices[position]) "${devices[position]} âœ“" else devices[position].toString()
        }
        return view
    }
}


package com.amazonaws.services.chime.sdkdemo.adapter

import android.content.Context
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseExpandableListAdapter
import android.widget.CheckedTextView
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.SpinnerItem
import com.amazonaws.services.chime.sdkdemo.data.TranscribeLanguageOption
import com.amazonaws.services.chime.sdkdemo.utils.inflate

class LanguageOptionsAdapter(
    private val context: Context,
    private val languageOptions: Map<String, List<SpinnerItem?>>,
    private val languageGroups: List<String>,
    private val languageOptionsSelected: Set<TranscribeLanguageOption>
) : BaseExpandableListAdapter() {

    override fun getGroupCount(): Int {
        return languageOptions.size
    }

    override fun getChildrenCount(listPosition: Int): Int {
        return languageOptions[languageGroups[listPosition]]?.size ?: 0
    }

    override fun getGroup(listPosition: Int): Any {
        return languageGroups[listPosition]
    }

    override fun getChild(listPosition: Int, expandedListPosition: Int): SpinnerItem? {
        return languageOptions[languageGroups[listPosition]]?.getOrNull(expandedListPosition)
    }

    override fun getGroupId(listPosition: Int): Long {
        return listPosition.toLong()
    }

    override fun getChildId(listPosition: Int, expandedListPosition: Int): Long {
        return expandedListPosition.toLong()
    }

    override fun hasStableIds(): Boolean {
        return false
    }

    override fun getGroupView(listPosition: Int, isExpanded: Boolean, convertView_: View?, parent: ViewGroup?): View? {
        var convertView: View? = convertView_
        val listTitle = getGroup(listPosition) as String
        if (convertView == null) {
            convertView = parent?.inflate(R.layout.row_language_option, false)
        }
        if (convertView != null) {
            val listTitleTextView: CheckedTextView = convertView.findViewById(R.id.checkedTextViewLanguageOptionRow)
            listTitleTextView.setTypeface(null, Typeface.BOLD)
            listTitleTextView.checkMarkDrawable = null
            listTitleTextView.text = listTitle
        }
        return convertView
    }

    override fun getChildView(listPosition: Int, expandedListPosition: Int, isLastChild: Boolean, convertView_: View?, ViewGroup: ViewGroup?): View? {
        val expandedListText = (getChild(listPosition, expandedListPosition) as SpinnerItem).value
        var convertView = convertView_
        if (convertView == null) {
            val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            convertView = layoutInflater.inflate(R.layout.row_language_option, null)
        }
        if (convertView != null) {
            val textView: CheckedTextView = convertView.findViewById(R.id.checkedTextViewLanguageOptionRow)
            val cell = languageOptions[languageGroups[listPosition]]?.get(expandedListPosition)?.let {
                TranscribeLanguageOption(listPosition, expandedListPosition, it)
            }
            textView.isChecked = languageOptionsSelected.contains(cell)
            textView.text = expandedListText
        }

        return convertView
    }

    override fun isChildSelectable(p0: Int, p1: Int): Boolean {
        return true
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.Message
import com.amazonaws.services.chime.sdkdemo.utils.inflate
import kotlinx.android.synthetic.main.row_message.view.messageText
import kotlinx.android.synthetic.main.row_message.view.messageTimestamp
import kotlinx.android.synthetic.main.row_message.view.senderName

class MessageAdapter(
    private val messages: Collection<Message>
) :
    RecyclerView.Adapter<MessageHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageHolder {
        val inflatedView = parent.inflate(R.layout.row_message, false)
        return MessageHolder(inflatedView)
    }

    override fun getItemCount(): Int {
        return messages.size
    }

    override fun onBindViewHolder(holder: MessageHolder, position: Int) {
        val message = messages.elementAt(position)
        holder.bindMessage(message)
    }
}

class MessageHolder(inflatedView: View) :
    RecyclerView.ViewHolder(inflatedView) {
    private var view: View = inflatedView

    fun bindMessage(message: Message) {
        view.senderName.text = message.senderName
        view.messageTimestamp.text = message.displayTime
        view.messageText.text = message.text
        view.messageText.contentDescription = message.text
        view.messageText.textAlignment =
            if (message.isLocal) View.TEXT_ALIGNMENT_TEXT_END else View.TEXT_ALIGNMENT_TEXT_START
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.MetricData
import com.amazonaws.services.chime.sdkdemo.utils.inflate
import kotlinx.android.synthetic.main.row_metric.view.*

class MetricAdapter(
    private val metricsList: Collection<MetricData>
) :
    RecyclerView.Adapter<MetricHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MetricHolder {
        val inflatedView = parent.inflate(R.layout.row_metric, false)
        return MetricHolder(inflatedView)
    }

    override fun getItemCount(): Int {
        return metricsList.size
    }

    override fun onBindViewHolder(holder: MetricHolder, position: Int) {
        holder.bindMetrics(metricsList.elementAt(position))
    }
}

class MetricHolder(inflatedView: View) :
    RecyclerView.ViewHolder(inflatedView) {

    private var view: View = inflatedView

    fun bindMetrics(metric: MetricData) {
        val name = metric.metricName
        val value = metric.metricValue.toString()
        view.metricName.text = name
        view.metricValue.text = value
        view.metricName.contentDescription = "$name metric"
        view.metricValue.contentDescription = "$name value"
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalStrength
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.internal.AttendeeStatus
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.data.RosterAttendee
import com.amazonaws.services.chime.sdkdemo.utils.inflate
import kotlinx.android.synthetic.main.row_roster.view.activeSpeakerIndicator
import kotlinx.android.synthetic.main.row_roster.view.attendeeName
import kotlinx.android.synthetic.main.row_roster.view.attendeeVolume

class RosterAdapter(
    private val roster: Collection<RosterAttendee>
) :
    RecyclerView.Adapter<RosterHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RosterHolder {
        val inflatedView = parent.inflate(R.layout.row_roster, false)
        return RosterHolder(inflatedView)
    }

    override fun getItemCount(): Int {
        return roster.size
    }

    override fun onBindViewHolder(holder: RosterHolder, position: Int) {
        val attendee = roster.elementAt(position)
        holder.bindAttendee(attendee)
    }
}

class RosterHolder(inflatedView: View) :
    RecyclerView.ViewHolder(inflatedView) {

    private var view: View = inflatedView

    fun bindAttendee(attendee: RosterAttendee) {
        val attendeeName = attendee.attendeeName
        view.attendeeName.text = attendeeName
        view.attendeeName.contentDescription = attendeeName
        view.activeSpeakerIndicator.visibility = if (attendee.isActiveSpeaker) View.VISIBLE else View.INVISIBLE
        view.activeSpeakerIndicator.contentDescription = if (attendee.isActiveSpeaker) "${attendee.attendeeName} Active" else ""

        if (attendee.attendeeStatus == AttendeeStatus.Joined) {
            if (attendee.signalStrength == SignalStrength.None ||
                    attendee.signalStrength == SignalStrength.Low
            ) {
                val drawable = if (attendee.volumeLevel == VolumeLevel.Muted) {
                    R.drawable.ic_microphone_poor_connectivity_dissabled
                } else {
                    R.drawable.ic_microphone_poor_connectivity
                }
                view.attendeeVolume.setImageResource(drawable)
                view.contentDescription = "$attendeeName Signal Strength Poor"
            } else {
                when (attendee.volumeLevel) {
                    VolumeLevel.Muted -> {
                        view.attendeeVolume.setImageResource(R.drawable.ic_microphone_disabled)
                        view.attendeeVolume.contentDescription = "$attendeeName Muted"
                    }
                    VolumeLevel.NotSpeaking -> {
                        view.attendeeVolume.setImageResource(R.drawable.ic_microphone_enabled)
                        view.attendeeVolume.contentDescription = "$attendeeName Not Speaking"
                    }
                    VolumeLevel.Low -> {
                        view.attendeeVolume.setImageResource(R.drawable.ic_microphone_audio_1)
                        view.attendeeVolume.contentDescription = "$attendeeName Speaking"
                    }
                    VolumeLevel.Medium -> {
                        view.attendeeVolume.setImageResource(R.drawable.ic_microphone_audio_2)
                        view.attendeeVolume.contentDescription = "$attendeeName Speaking"
                    }
                    VolumeLevel.High -> {
                        view.attendeeVolume.setImageResource(R.drawable.ic_microphone_audio_3)
                        view.attendeeVolume.contentDescription = "$attendeeName Speaking"
                    }
                }
            }
            view.attendeeVolume.visibility = View.VISIBLE
        } else {
            view.attendeeVolume.visibility = View.GONE
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.adapter

import android.content.Context
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoPauseState
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoPriority
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoResolution
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoScalingType
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.SegmentationProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.backgroundblur.BackgroundBlurVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.backgroundfilter.backgroundreplacement.BackgroundReplacementVideoFrameProcessor
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.CameraCaptureSource
import com.amazonaws.services.chime.sdk.meetings.device.MediaDeviceType
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.activity.MeetingActivity
import com.amazonaws.services.chime.sdkdemo.data.VideoCollectionTile
import com.amazonaws.services.chime.sdkdemo.model.MeetingModel
import com.amazonaws.services.chime.sdkdemo.utils.inflate
import com.amazonaws.services.chime.sdkdemo.utils.isLandscapeMode
import kotlinx.android.synthetic.main.item_video.view.attendee_name
import kotlinx.android.synthetic.main.item_video.view.on_tile_button
import kotlinx.android.synthetic.main.item_video.view.poor_connection_message
import kotlinx.android.synthetic.main.item_video.view.video_config_button
import kotlinx.android.synthetic.main.item_video.view.video_filter_button
import kotlinx.android.synthetic.main.item_video.view.video_surface

class VideoAdapter(
    private val videoCollectionTiles: Collection<VideoCollectionTile>,
    private val meetingModel: MeetingModel,
    private val audioVideoFacade: AudioVideoFacade,
    private val cameraCaptureSource: CameraCaptureSource?,
    private val backgroundBlurVideoFrameProcessor: BackgroundBlurVideoFrameProcessor?,
    private val backgroundReplacementVideoFrameProcessor: BackgroundReplacementVideoFrameProcessor?,
    private val context: Context?,
    private val logger: Logger
) : RecyclerView.Adapter<VideoHolder>() {
    private lateinit var tabContentLayout: ConstraintLayout
    private val VIDEO_ASPECT_RATIO_16_9 = 0.5625

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoHolder {
        tabContentLayout = (context as MeetingActivity).findViewById(R.id.constraintLayout)
        val inflatedView = parent.inflate(R.layout.item_video, false)
        return VideoHolder(
            context,
            inflatedView,
            audioVideoFacade,
            meetingModel,
            logger,
            cameraCaptureSource,
            backgroundBlurVideoFrameProcessor,
            backgroundReplacementVideoFrameProcessor
        )
    }

    override fun getItemCount(): Int {
        return videoCollectionTiles.size
    }

    override fun onBindViewHolder(holder: VideoHolder, position: Int) {
        val videoCollectionTile = videoCollectionTiles.elementAt(position)
        holder.bindVideoTile(videoCollectionTile)
        context?.let {
            if (!videoCollectionTile.videoTileState.isContent) {
                val viewportWidth = tabContentLayout.width
                if (isLandscapeMode(context)) {
                    holder.tileContainer.layoutParams.width = viewportWidth / 2
                } else {
                    holder.tileContainer.layoutParams.height =
                        (VIDEO_ASPECT_RATIO_16_9 * viewportWidth).toInt()
                }
            }
            val videoRenderView = holder.itemView.video_surface
            videoRenderView.scalingType = VideoScalingType.AspectFit
            videoRenderView.hardwareScaling = false
        }
    }
}

class VideoHolder(
    private val context: Context,
    private val view: View,
    private val audioVideo: AudioVideoFacade,
    private val meetingModel: MeetingModel,
    private val logger: Logger,
    private val cameraCaptureSource: CameraCaptureSource?,
    private val backgroundBlurVideoFrameProcessor: BackgroundBlurVideoFrameProcessor?,
    private val backgroundReplacementVideoFrameProcessor: BackgroundReplacementVideoFrameProcessor?
) : RecyclerView.ViewHolder(view) {

    val tileContainer: ConstraintLayout = view.findViewById(R.id.tile_container)
    private val TAG = "VideoHolder"

    init {
        view.video_surface.logger = logger
    }

    fun bindVideoTile(videoCollectionTile: VideoCollectionTile) {
        audioVideo.bindVideoView(view.video_surface, videoCollectionTile.videoTileState.tileId)
        // Save the bound VideoRenderView in order to explicitly control the visibility of SurfaceView.
        // This is to bypass the issue where we cannot hide a SurfaceView that overlaps with another one.
        videoCollectionTile.videoRenderView = view.video_surface
        videoCollectionTile.pauseMessageView = view.poor_connection_message

        if (videoCollectionTile.videoTileState.isContent) {
            view.video_surface.contentDescription = "ScreenTile"
        } else {
            view.video_surface.contentDescription = "${videoCollectionTile.attendeeName} VideoTile"
        }
        if (videoCollectionTile.videoTileState.isLocalTile) {
            view.on_tile_button.setImageResource(R.drawable.ic_switch_camera)
            view.attendee_name.visibility = View.GONE
            view.on_tile_button.visibility = View.VISIBLE

            if (SegmentationProcessor.isMachineLearningLibraryLoaded) {
                view.video_filter_button.setImageResource(R.drawable.button_more)
                view.video_filter_button.visibility = View.VISIBLE
            } else {
                view.video_filter_button.visibility = View.INVISIBLE
            }
            // To facilitate demoing and testing both use cases, we account for both our external
            // camera and the camera managed by the facade. Actual applications should
            // only use one or the other
            updateLocalVideoMirror()
            view.on_tile_button.setOnClickListener {
                if (audioVideo.getActiveCamera() != null) {
                    audioVideo.switchCamera()
                } else {
                    cameraCaptureSource?.switchCamera()
                }
                updateLocalVideoMirror()
            }
            if (SegmentationProcessor.isMachineLearningLibraryLoaded) {
                view.video_filter_button.setOnClickListener {
                    showVideoFilterPopup(view.video_filter_button)
                }
            }
        } else {
            view.video_surface.mirror = false
            view.attendee_name.text = videoCollectionTile.attendeeName
            view.attendee_name.visibility = View.VISIBLE
            view.on_tile_button.visibility = View.VISIBLE
            view.video_filter_button.visibility = View.INVISIBLE
            when (videoCollectionTile.videoTileState.pauseState) {
                VideoPauseState.Unpaused ->
                    view.on_tile_button.setImageResource(R.drawable.ic_pause_video)
                VideoPauseState.PausedByUserRequest ->
                    view.on_tile_button.setImageResource(R.drawable.ic_resume_video)
                VideoPauseState.PausedForPoorConnection ->
                    view.poor_connection_message.visibility = View.VISIBLE
            }

            view.on_tile_button.setOnClickListener {
                val tileId = videoCollectionTile.videoTileState.tileId
                if (videoCollectionTile.videoTileState.pauseState == VideoPauseState.Unpaused) {
                    audioVideo.pauseRemoteVideoTile(tileId)
                    meetingModel.userPausedVideoTileIds.add(tileId)
                    view.on_tile_button.setImageResource(R.drawable.ic_resume_video)
                } else {
                    audioVideo.resumeRemoteVideoTile(tileId)
                    meetingModel.userPausedVideoTileIds.remove(tileId)
                    view.on_tile_button.setImageResource(R.drawable.ic_pause_video)
                }
            }

            view.video_surface.setOnClickListener {
                val attendeeId = videoCollectionTile.videoTileState.attendeeId
                showPriorityPopup(view.on_tile_button, attendeeId)
            }
        }

        view.video_config_button.setOnClickListener {
            val attendeeId = videoCollectionTile.videoTileState.attendeeId
            showResolutionPopup(view.video_config_button, attendeeId)
        }
    }

    private fun showPriorityPopup(view: View, attendeeId: String) {
        val popup = PopupMenu(context, view)
        popup.inflate(R.menu.priority_popup_menu)
        popup.setOnMenuItemClickListener { item: MenuItem? ->
            val priority = when (item?.itemId) {
                R.id.highest -> {
                    VideoPriority.Highest
                }
                R.id.high -> {
                    VideoPriority.High
                }
                R.id.medium -> {
                    VideoPriority.Medium
                }
                R.id.low -> {
                    VideoPriority.Low
                }
                R.id.lowest -> {
                    VideoPriority.Lowest
                }
                else -> {
                    VideoPriority.Lowest
                }
            }

            val updatedSource = mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()
            for (source in meetingModel.getRemoteVideoSourceConfigurations()) {
                if (source.key.attendeeId == attendeeId) {
                    val resolution: VideoResolution = source.value.targetResolution
                    val config = VideoSubscriptionConfiguration(priority, resolution)
                    meetingModel.setRemoteVideoSourceConfigurations(source.key, config)
                    updatedSource[source.key] = source.value
                }
            }
            audioVideo.updateVideoSourceSubscriptions(
                meetingModel.getRemoteVideoSourceConfigurations(),
                emptyArray()
            )
            true
        }
        popup.show()
    }

    private fun showResolutionPopup(view: View, attendeeId: String) {
        val popup = PopupMenu(context, view)
        popup.inflate(R.menu.resolution_popup_menu)
        popup.setOnMenuItemClickListener { item: MenuItem? ->
            val resolution = when (item?.itemId) {
                R.id.high -> {
                    VideoResolution.High
                }
                R.id.medium -> {
                    VideoResolution.Medium
                }
                R.id.low -> {
                    VideoResolution.Low
                }
                else -> {
                    VideoResolution.Low
                }
            }

            val updatedSource = mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()
            for (source in meetingModel.getRemoteVideoSourceConfigurations()) {
                if (source.key.attendeeId == attendeeId) {
                    val priority: VideoPriority = source.value.priority
                    val config = VideoSubscriptionConfiguration(priority, resolution)
                    meetingModel.setRemoteVideoSourceConfigurations(source.key, config)
                    updatedSource[source.key] = source.value
                }
            }

            audioVideo.updateVideoSourceSubscriptions(
                updatedSource,
                emptyArray()
            )
            true
        }
        popup.show()
    }

    private fun showVideoFilterPopup(view: View) {
        val popup = PopupMenu(context, view)
        popup.setOnMenuItemClickListener { item: MenuItem? ->
            when (item?.itemId) {
                R.id.backgroundBlur -> {
                    toggleVideoBackgroundBlur()
                }
                R.id.backgroundReplacement -> {
                    toggleVideoBackgroundReplacement()
                }
                else -> {}
            }
            true
        }
        popup.inflate(R.menu.video_filter_popup_menu)
        if (meetingModel.isUsingBackgroundBlur) {
            popup.menu.getItem(0).setTitle(R.string.disable_background_blur)
        }
        if (meetingModel.isUsingBackgroundReplacement) {
            popup.menu.getItem(1).setTitle(R.string.disable_background_replacement)
        }
        popup.show()
    }

    private fun toggleVideoBackgroundBlur() {
        if (!meetingModel.isUsingCameraCaptureSource) {
            logger.warn(TAG, "Cannot toggle filter without using custom camera capture source.")
            Toast.makeText(
                context,
                R.string.user_notification_filter_custom_source_error,
                Toast.LENGTH_SHORT
            ).show()
            return
        }
        if (meetingModel.isUsingBackgroundReplacement || meetingModel.isUsingCpuVideoProcessor || meetingModel.isUsingGpuVideoProcessor) {
            logger.warn(
                TAG,
                "Cannot toggle background blur filter when other filter is enabled."
            )
            Toast.makeText(
                context,
                R.string.user_notification_background_blur_custom_source_error,
                Toast.LENGTH_SHORT
            ).show()
            return
        }
        logger.info(
            TAG,
            "Toggling Background Blur from ${meetingModel.isUsingBackgroundBlur} to ${!meetingModel.isUsingBackgroundBlur}."
        )
        meetingModel.isUsingBackgroundBlur = !meetingModel.isUsingBackgroundBlur
        if (meetingModel.isLocalVideoStarted) {
            startLocalVideo()
        }
    }

    private fun toggleVideoBackgroundReplacement() {
        if (!meetingModel.isUsingCameraCaptureSource) {
            logger.warn(TAG, "Cannot toggle filter without using custom camera capture source.")
            Toast.makeText(
                context,
                R.string.user_notification_filter_custom_source_error,
                Toast.LENGTH_SHORT
            ).show()
            return
        }
        if (meetingModel.isUsingBackgroundBlur || meetingModel.isUsingCpuVideoProcessor || meetingModel.isUsingGpuVideoProcessor) {
            logger.warn(
                TAG,
                "Cannot toggle background replacement filter when other filter is enabled."
            )
            Toast.makeText(
                context,
                R.string.user_notification_background_replacement_custom_source_error,
                Toast.LENGTH_SHORT
            ).show()
            return
        }
        logger.info(
            TAG,
            "Toggling Background Replacement from ${meetingModel.isUsingBackgroundReplacement} to ${!meetingModel.isUsingBackgroundReplacement}."
        )
        meetingModel.isUsingBackgroundReplacement = !meetingModel.isUsingBackgroundReplacement
        if (meetingModel.isLocalVideoStarted) {
            startLocalVideo()
        }
    }

    private fun startLocalVideo() {
        meetingModel.isLocalVideoStarted = true
        if (meetingModel.isUsingCameraCaptureSource) {
            when {
                meetingModel.isUsingBackgroundBlur -> {
                    if (backgroundBlurVideoFrameProcessor != null) {
                        cameraCaptureSource?.addVideoSink(backgroundBlurVideoFrameProcessor)
                        audioVideo.startLocalVideo(backgroundBlurVideoFrameProcessor)
                    }
                }
                meetingModel.isUsingBackgroundReplacement -> {
                    if (backgroundReplacementVideoFrameProcessor != null) {
                        cameraCaptureSource?.addVideoSink(backgroundReplacementVideoFrameProcessor)
                        audioVideo.startLocalVideo(backgroundReplacementVideoFrameProcessor)
                    }
                }
                else -> {
                    if (cameraCaptureSource != null) {
                        audioVideo.startLocalVideo(cameraCaptureSource)
                    }
                }
            }
            cameraCaptureSource?.start()
        } else {
            audioVideo.startLocalVideo()
        }
    }

    private fun updateLocalVideoMirror() {
        view.video_surface.mirror =
                // If we are using internal source, base mirror state off that device type
            (audioVideo.getActiveCamera()?.type == MediaDeviceType.VIDEO_FRONT_CAMERA ||
                    // Otherwise (audioVideo.getActiveCamera() == null) use the device type of our external/custom camera capture source
                    (audioVideo.getActiveCamera() == null && cameraCaptureSource?.device?.type == MediaDeviceType.VIDEO_FRONT_CAMERA))
    }
}


package com.amazonaws.services.chime.sdkdemo.adapter

import androidx.recyclerview.widget.DiffUtil
import com.amazonaws.services.chime.sdkdemo.data.VideoCollectionTile

class VideoDiffCallback(
    private val oldList: List<VideoCollectionTile>,
    private val newList: List<VideoCollectionTile>
) :
    DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].videoTileState.attendeeId == newList[newItemPosition].videoTileState.attendeeId
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldTileState = oldList[oldItemPosition].videoTileState
        val newTileState = newList[newItemPosition].videoTileState

        return oldTileState.attendeeId == newTileState.attendeeId &&
                oldTileState.pauseState == newTileState.pauseState &&
                oldTileState.videoStreamContentWidth == newTileState.videoStreamContentWidth &&
                oldTileState.videoStreamContentHeight == newTileState.videoStreamContentHeight
    }
}


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

package com.amazonaws.services.chime.sdkdemo.data

import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptItem

data class Caption(
    val speakerName: String?,
    val isPartial: Boolean,
    val content: String,
    val items: Array<TranscriptItem>? = null,
    val entities: MutableSet<String>? = null
)


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

package com.amazonaws.services.chime.sdkdemo.data

import com.amazonaws.services.chime.sdk.meetings.session.Attendee
import com.amazonaws.services.chime.sdk.meetings.session.Meeting
import com.google.gson.annotations.SerializedName

data class JoinMeetingResponse(
    @SerializedName("JoinInfo") val joinInfo: MeetingInfo
)

data class MeetingInfo(
    @SerializedName("Meeting") val meetingResponse: MeetingResponse,
    @SerializedName("Attendee") val attendeeResponse: AttendeeResponse,
    @SerializedName("PrimaryExternalMeetingId") val primaryExternalMeetingId: String
)

data class MeetingResponse(
    @SerializedName("Meeting") val meeting: Meeting
)

data class AttendeeResponse(
    @SerializedName("Attendee") val attendee: Attendee
)


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

package com.amazonaws.services.chime.sdkdemo.data

import java.text.SimpleDateFormat
import java.util.Date

data class Message(
    val senderName: String,
    val timestamp: Long,
    val text: String,
    val isLocal: Boolean
) {
    val displayTime: String
        get() {
            return SimpleDateFormat("HH:mm").format(Date(timestamp))
        }
}


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

package com.amazonaws.services.chime.sdkdemo.data

data class MetricData(
    val metricName: String,
    val metricValue: String
)


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

package com.amazonaws.services.chime.sdkdemo.data

import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalStrength
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.internal.AttendeeStatus

data class RosterAttendee(
    val attendeeId: String,
    val attendeeName: String,
    val volumeLevel: VolumeLevel = VolumeLevel.NotSpeaking,
    val signalStrength: SignalStrength = SignalStrength.High,
    val isActiveSpeaker: Boolean = false,
    val attendeeStatus: AttendeeStatus = AttendeeStatus.Joined
)


package com.amazonaws.services.chime.sdkdemo.data

data class SpinnerItem(val spinnerText: String?, val value: String) {
    override fun toString(): String {
        return value
    }
}

data class TranscribeLanguageOption(
    val languageGroupIndex: Int,
    val languageCodeIndex: Int,
    val transcribeLanguage: SpinnerItem
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TranscribeLanguageOption

        if (languageGroupIndex != other.languageGroupIndex && languageCodeIndex != other.languageCodeIndex)
            return false

        return true
    }

    override fun hashCode(): Int {
        var result = languageGroupIndex
        result = 31 * result + languageCodeIndex
        return result
    }
}

data class TranscriptionStreamParams(
    val contentIdentificationType: String?,
    val contentRedactionType: String?,
    val enablePartialResultsStabilization: Boolean,
    val partialResultsStability: String?,
    val piiEntityTypes: String?,
    val languageModelName: String?,
    val identifyLanguage: Boolean?,
    val languageOptions: String?,
    val preferredLanguage: String?
)


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package com.amazonaws.services.chime.sdkdemo.data

import androidx.constraintlayout.widget.ConstraintLayout
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.DefaultVideoRenderView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileState

data class VideoCollectionTile(
    val attendeeName: String,
    val videoTileState: VideoTileState
) {
    var videoRenderView: DefaultVideoRenderView? = null
    var pauseMessageView: ConstraintLayout? = null

    fun setRenderViewVisibility(visibility: Int) {
        videoRenderView?.visibility = visibility
    }

    fun setPauseMessageVisibility(visibility: Int) {
        pauseMessageView?.visibility = visibility
    }
}


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

package com.amazonaws.services.chime.sdkdemo.device

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice

/**
 * In order to support 23 or below we'll need to manually handle active audio device locally.
 */
class AudioDeviceManager(private val audioVideo: AudioVideoFacade) {
    // Handle active audio device for 21-23 since getActiveAudioDevice is only supported 24+
    private var currentActiveAudioDevice: MediaDevice? = null

    val activeAudioDevice: MediaDevice? get() = currentActiveAudioDevice

    /**
     * Set active device whenever chooseAudioDevice is called.
     */
    fun setActiveAudioDevice(mediaDevice: MediaDevice) {
        currentActiveAudioDevice = mediaDevice
    }

    /**
     * Reconfigure active device based on priority.
     * Current priority is bluetooth -> wired headset -> speakerphone -> earpiece
     */
    fun reconfigureActiveAudioDevice() {
        val devices = audioVideo.listAudioDevices().sortedBy { it.order }
        if (devices.isNotEmpty()) {
            if (devices[0] == currentActiveAudioDevice) return
            audioVideo.chooseAudioDevice(devices[0])
            setActiveAudioDevice(devices[0])
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.device

import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.media.projection.MediaProjection
import android.os.Build
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.CaptureSourceObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.DefaultScreenCaptureSource
import com.amazonaws.services.chime.sdkdemo.service.ScreenCaptureService

/**
 * [ScreenShareManager] is a wrapper of [ContentShareSource]. It manages
 * [DefaultScreenCaptureSource] and foreground service which required for [MediaProjection]
 * starting [Build.VERSION_CODES.Q].
 */
class ScreenShareManager(
    private val screenCaptureSource: DefaultScreenCaptureSource,
    private val context: Context
) : ContentShareSource() {
    override var videoSource: VideoSource? = screenCaptureSource

    var screenCaptureConnectionService: ServiceConnection? = null

    fun start() = screenCaptureSource.start()

    fun stop(isBound: Boolean = false) {
        context.stopService(Intent(context, ScreenCaptureService::class.java))
        screenCaptureSource.stop()
        screenCaptureConnectionService?.let {
            if (isBound) context.unbindService(it)
        }
    }

    fun release() = screenCaptureSource.release()

    fun addObserver(observer: CaptureSourceObserver) = screenCaptureSource.addCaptureSourceObserver(observer)

    fun removeObserver(observer: CaptureSourceObserver) = screenCaptureSource.removeCaptureSourceObserver(observer)
}


package com.amazonaws.services.chime.sdkdemo.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.model.DebugSettingsViewModel
import kotlinx.android.synthetic.main.fragment_debug_settings.view.*

class DebugSettingsFragment : DialogFragment() {
    private lateinit var debugSettingsViewModel: DebugSettingsViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout to use as embedded fragment
        return inflater.inflate(R.layout.fragment_debug_settings, container, false)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(STYLE_NO_TITLE, android.R.style.Theme_DeviceDefault_Light_NoActionBar)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        debugSettingsViewModel = ViewModelProvider(requireActivity()).get(DebugSettingsViewModel::class.java)
        view.endpointUrlEditText.setText(debugSettingsViewModel.endpointUrl.value)
        view.primaryMeetingIdEditText.setText(debugSettingsViewModel.primaryMeetingId.value)
        setupClickListeners(view)
    }

    private fun setupClickListeners(view: View) {
        view.saveButton.setOnClickListener {
            debugSettingsViewModel.sendEndpointUrl(view.endpointUrlEditText.text.toString())
            debugSettingsViewModel.sendPrimaryMeetingId(view.primaryMeetingIdEditText.text.toString())
            dismiss()
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.fragment

import android.content.Context
import android.hardware.camera2.CameraManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.Spinner
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.DefaultVideoRenderView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.CameraCaptureSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture.VideoCaptureFormat
import com.amazonaws.services.chime.sdk.meetings.device.DeviceChangeObserver
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdk.meetings.device.MediaDeviceType
import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger
import com.amazonaws.services.chime.sdk.meetings.utils.logger.LogLevel
import com.amazonaws.services.chime.sdkdemo.R
import com.amazonaws.services.chime.sdkdemo.activity.HomeActivity
import com.amazonaws.services.chime.sdkdemo.activity.MeetingActivity
import com.amazonaws.services.chime.sdkdemo.utils.isLandscapeMode
import java.lang.ClassCastException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class DeviceManagementFragment : Fragment(), DeviceChangeObserver {
    private val logger = ConsoleLogger(LogLevel.INFO)
    private val uiScope = CoroutineScope(Dispatchers.Main)
    private val audioDevices = mutableListOf<MediaDevice>()
    private val videoDevices = mutableListOf<MediaDevice>()
    private val videoFormats = mutableListOf<VideoCaptureFormat>()

    private lateinit var cameraManager: CameraManager

    private lateinit var listener: DeviceManagementEventListener
    private lateinit var audioVideo: AudioVideoFacade
    private lateinit var cameraCaptureSource: CameraCaptureSource
    private lateinit var videoPreview: DefaultVideoRenderView

    private val TAG = "DeviceManagementFragment"

    private lateinit var audioDeviceSpinner: Spinner
    private lateinit var audioDeviceArrayAdapter: ArrayAdapter<MediaDevice>
    private lateinit var videoDeviceSpinner: Spinner
    private lateinit var videoDeviceArrayAdapter: ArrayAdapter<MediaDevice>
    private lateinit var videoFormatSpinner: Spinner
    private lateinit var videoFormatArrayAdapter: ArrayAdapter<VideoCaptureFormat>

    private val VIDEO_ASPECT_RATIO_16_9 = 0.5625

    private val AUDIO_DEVICE_SPINNER_INDEX_KEY = "audioDeviceSpinnerIndex"
    private val VIDEO_DEVICE_SPINNER_INDEX_KEY = "videoDeviceSpinnerIndex"
    private val VIDEO_FORMAT_SPINNER_INDEX_KEY = "videoFormatSpinnerIndex"

    private val MAX_VIDEO_FORMAT_HEIGHT = 800
    private val MAX_VIDEO_FORMAT_FPS = 30

    companion object {
        fun newInstance(meetingId: String, name: String, audioVideoConfig: AudioVideoConfiguration): DeviceManagementFragment {
            val fragment = DeviceManagementFragment()

            fragment.arguments = bundleOf(
                HomeActivity.MEETING_ID_KEY to meetingId,
                HomeActivity.NAME_KEY to name,
                HomeActivity.AUDIO_MODE_KEY to audioVideoConfig.audioMode.value
            )
            return fragment
        }
    }

    interface DeviceManagementEventListener {
        fun onJoinMeetingClicked()
        fun onCachedDeviceSelected(mediaDevice: MediaDevice)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        if (context is DeviceManagementEventListener) {
            listener = context
        } else {
            logger.error(TAG, "$context must implement DeviceManagementEventListener.")
            throw ClassCastException("$context must implement DeviceManagementEventListener.")
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_device_management, container, false)
        val context = activity as Context

        val meetingId = arguments?.getString(HomeActivity.MEETING_ID_KEY)
        val name = arguments?.getString(HomeActivity.NAME_KEY)
        val audioMode = arguments?.getInt(HomeActivity.AUDIO_MODE_KEY)?.let { intValue ->
            AudioMode.from(intValue, defaultAudioMode = AudioMode.Stereo48K)
        } ?: AudioMode.Stereo48K
        audioVideo = (activity as MeetingActivity).getAudioVideo()

        val displayedText = getString(R.string.preview_meeting_info, meetingId, name)
        view.findViewById<TextView>(R.id.textViewMeetingPreview)?.text = displayedText

        view.findViewById<Button>(R.id.buttonJoin)?.setOnClickListener {
            listener.onJoinMeetingClicked()
        }

        // Note we call isSelected and setSelection before setting onItemSelectedListener
        // so that we can control the first time the spinner is set and use previous values
        // if they exist (i.e. before rotation). We will set them after lists are populated.

        audioDeviceSpinner = view.findViewById(R.id.spinnerAudioDevice)
        audioDeviceArrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, audioDevices)
        audioDeviceSpinner.adapter = audioDeviceArrayAdapter
        audioDeviceSpinner.isSelected = false
        audioDeviceSpinner.setSelection(0, true)
        audioDeviceSpinner.onItemSelectedListener = onAudioDeviceSelected

        videoDeviceSpinner = view.findViewById(R.id.spinnerVideoDevice)
        videoDeviceArrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, videoDevices)
        videoDeviceSpinner.adapter = videoDeviceArrayAdapter
        videoDeviceSpinner.isSelected = false
        videoDeviceSpinner.setSelection(0, true)
        videoDeviceSpinner.onItemSelectedListener = onVideoDeviceSelected

        videoFormatSpinner = view.findViewById(R.id.spinnerVideoFormat)
        videoFormatArrayAdapter =
                ArrayAdapter(context, android.R.layout.simple_spinner_item, videoFormats)
        videoFormatSpinner.adapter = videoFormatArrayAdapter
        videoFormatSpinner.isSelected = false
        videoFormatSpinner.setSelection(0, true)
        videoFormatSpinner.onItemSelectedListener = onVideoFormatSelected

        audioVideo.addDeviceChangeObserver(this)

        cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager

        cameraCaptureSource = (activity as MeetingActivity).getCameraCaptureSource()

        view.findViewById<DefaultVideoRenderView>(R.id.videoPreview)?.let {
            val displayMetrics = context.resources.displayMetrics
            val width =
                    if (isLandscapeMode(context)) displayMetrics.widthPixels / 2 else displayMetrics.widthPixels
            val height = (width * VIDEO_ASPECT_RATIO_16_9).toInt()
            it.layoutParams.width = width
            it.layoutParams.height = height

            it.logger = logger
            it.init((activity as MeetingActivity).getEglCoreFactory())
            cameraCaptureSource.addVideoSink(it)
            videoPreview = it
        }

        uiScope.launch {
            populateDeviceList(audioVideo.listAudioDevices(), audioDevices, audioDeviceArrayAdapter)
            populateDeviceList(MediaDevice.listVideoDevices(cameraManager), videoDevices, videoDeviceArrayAdapter)
            cameraCaptureSource.device ?.let {
                populateVideoFormatList(MediaDevice.listSupportedVideoCaptureFormats(cameraManager, it))
            }

            videoPreview.mirror =
                    cameraCaptureSource.device?.type == MediaDeviceType.VIDEO_FRONT_CAMERA

            var audioDeviceSpinnerIndex = 0
            var videoDeviceSpinnerIndex = 0
            var videoFormatSpinnerIndex = 0
            if (savedInstanceState != null) {
                audioDeviceSpinnerIndex = savedInstanceState.getInt(AUDIO_DEVICE_SPINNER_INDEX_KEY, 0)
                videoDeviceSpinnerIndex = savedInstanceState.getInt(VIDEO_DEVICE_SPINNER_INDEX_KEY, 0)
                videoFormatSpinnerIndex = savedInstanceState.getInt(VIDEO_FORMAT_SPINNER_INDEX_KEY, 0)
            }

            audioDeviceSpinner.setSelection(audioDeviceSpinnerIndex)
            videoDeviceSpinner.setSelection(videoDeviceSpinnerIndex)
            videoFormatSpinner.setSelection(videoFormatSpinnerIndex)

            // Setting the selection won't immediately callback, so we need to explicitly set the values
            // of the camera capturer before starting it
            cameraCaptureSource.device = videoDeviceSpinner.getItemAtPosition(videoDeviceSpinnerIndex) as MediaDevice
            videoPreview.mirror = cameraCaptureSource.device?.type == MediaDeviceType.VIDEO_FRONT_CAMERA
            cameraCaptureSource.format = videoFormatSpinner.getItemAtPosition(videoFormatSpinnerIndex) as VideoCaptureFormat

            cameraCaptureSource.start()
        }

        return view
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(AUDIO_DEVICE_SPINNER_INDEX_KEY, audioDeviceSpinner.selectedItemPosition)
        outState.putInt(VIDEO_DEVICE_SPINNER_INDEX_KEY, videoDeviceSpinner.selectedItemPosition)
        outState.putInt(VIDEO_FORMAT_SPINNER_INDEX_KEY, videoFormatSpinner.selectedItemPosition)
    }

    override fun onDestroy() {
        super.onDestroy()

        cameraCaptureSource.stop()
        cameraCaptureSource.removeVideoSink(videoPreview)
        videoPreview.release()
        audioVideo.removeDeviceChangeObserver(this)
    }

    private val onAudioDeviceSelected = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            listener.onCachedDeviceSelected(parent?.getItemAtPosition(position) as MediaDevice)
        }

        // Abstract, requires implementation
        override fun onNothingSelected(parent: AdapterView<*>?) {
        }
    }

    private val onVideoDeviceSelected = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            cameraCaptureSource.device = parent?.getItemAtPosition(position) as MediaDevice

            videoPreview.mirror =
                    cameraCaptureSource.device?.type == MediaDeviceType.VIDEO_FRONT_CAMERA
        }

        // Abstract, requires implementation
        override fun onNothingSelected(parent: AdapterView<*>?) {
        }
    }

    private val onVideoFormatSelected = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            cameraCaptureSource.format = parent?.getItemAtPosition(position) as VideoCaptureFormat
        }

        // Abstract, requires implementation
        override fun onNothingSelected(parent: AdapterView<*>?) {
        }
    }

    private fun populateDeviceList(newDeviceList: List<MediaDevice>, currentDeviceList: MutableList<MediaDevice>, adapter: ArrayAdapter<MediaDevice>) {
        currentDeviceList.clear()
        currentDeviceList.addAll(
                newDeviceList.filter {
                    it.type != MediaDeviceType.OTHER
                }.sortedBy { it.order }
        )
        adapter.notifyDataSetChanged()
        if (currentDeviceList.isNotEmpty()) {
            listener.onCachedDeviceSelected(currentDeviceList[0])
        }
    }

    private fun populateVideoFormatList(freshVideoCaptureFormatList: List<VideoCaptureFormat>) {
        videoFormats.clear()

        val filteredFormats = freshVideoCaptureFormatList.filter {
            it.height <= MAX_VIDEO_FORMAT_HEIGHT &&
            it.maxFps <= MAX_VIDEO_FORMAT_FPS
        }

        for (format in filteredFormats) {
            videoFormats.add(VideoCaptureFormat(format.width, format.height, format.maxFps))
        }
        videoFormatArrayAdapter.notifyDataSetChanged()
    }

    override fun onAudioDeviceChanged(freshAudioDeviceList: List<MediaDevice>) {
        populateDeviceList(freshAudioDeviceList, audioDevices, audioDeviceArrayAdapter)
    }
}