/*
 * 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.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
import android.widget.CheckedTextView
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ExpandableListAdapter
import android.widget.ExpandableListView
import android.widget.ExpandableListView.OnChildClickListener
import android.widget.Spinner
import android.widget.TextView
import androidx.fragment.app.Fragment
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.adapter.LanguageOptionsAdapter
import com.amazonaws.services.chime.sdkdemo.data.SpinnerItem
import com.amazonaws.services.chime.sdkdemo.data.TranscribeLanguageOption
import java.lang.ClassCastException
import kotlinx.android.synthetic.main.fragment_transcription_config.checkboxCustomLanguageModel
import kotlinx.android.synthetic.main.fragment_transcription_config.checkboxPHIContentIdentification
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class TranscriptionConfigFragment : Fragment() {
    private val logger = ConsoleLogger(LogLevel.INFO)
    private val uiScope = CoroutineScope(Dispatchers.Main)
    private var languages = mutableListOf<SpinnerItem>()
    private var regions = mutableListOf<SpinnerItem>()

    private val languageGroups = arrayListOf<String>(
        "English",
        "Spanish",
        "French",
        "Italian",
        "German",
        "Portuguese",
        "Japanese",
        "Korean",
        "Chinese"
    )

    private val languagesMap = mapOf<String, String>(
        "en-US" to "US English (en-US)",
        "es-US" to "US Spanish (es-US)",
        "en-GB" to "British English (en-GB)",
        "en-AU" to "Australian English (en-AU)",
        "fr-CA" to "Canadian French (fr-CA)",
        "fr-FR" to "French (fr-FR)",
        "it-IT" to "Italian (it-IT)",
        "de-DE" to "German (de-DE)",
        "pt-BR" to "Brazilian Portuguese (pt-BR)",
        "ja-JP" to "Japanese (ja-JP)",
        "ko-KR" to "Korean (ko-KR)",
        "zh-CN" to "Mandarin Chinese - Mainland (zh-CN)"
    )

    private val languageGroupMapping: List<Pair<String, String>> = listOf<Pair<String, String>>(
        Pair("English", "en-US"),
        Pair("English", "en-GB"),
        Pair("English", "en-AU"),
        Pair("Spanish", "es-US"),
        Pair("French", "fr-CA"),
        Pair("French", "fr-FR"),
        Pair("Italian", "it-IT"),
        Pair("German", "de-DE"),
        Pair("Portuguese", "pt-BR"),
        Pair("Japanese", "ja-JP"),
        Pair("Korean", "ko-KR"),
        Pair("Chinese", "zh-CN")
    )

    private val regionsMap = mapOf<String, String>(
        "auto" to "Auto",
        "" to "Not specified",
        "ap-northeast-1" to "Japan (Tokyo)",
        "ap-northeast-2" to "South Korea (Seoul)",
        "ap-southeast-2" to "Australia (Sydney)",
        "ca-central-1" to "Canada",
        "eu-central-1" to "Germany (Frankfurt)",
        "eu-west-1" to "Ireland",
        "eu-west-2" to "United Kingdom (London)",
        "sa-east-1" to "Brazil (SÃ£o Paulo)",
        "us-east-1" to "United States (N. Virginia)",
        "us-east-2" to "United States (Ohio)",
        "us-west-2" to "United States (Oregon)"
    )

    private val transcribeLanguages: List<SpinnerItem> = arrayOf(
        "en-US",
        "es-US",
        "en-GB",
        "en-AU",
        "fr-CA",
        "fr-FR",
        "it-IT",
        "de-DE",
        "pt-BR",
        "ja-JP",
        "ko-KR",
        "zh-CN"
    ).mapNotNull { l ->
        languagesMap[l]?.let {
            SpinnerItem(l, it)
        }
    }

    private val transcribeRegions: List<SpinnerItem> = arrayOf(
        "auto",
        "",
        "ap-northeast-1",
        "ap-northeast-2",
        "ap-southeast-2",
        "ca-central-1",
        "eu-central-1",
        "eu-west-1",
        "eu-west-2",
        "sa-east-1",
        "us-east-1",
        "us-east-2",
        "us-west-2"
    ).mapNotNull { l ->
        regionsMap[l]?.let {
            SpinnerItem(l, it)
        }
    }

    private val transcribePartialResultStabilizationValues: List<SpinnerItem> = mapOf<String?, String>(
        null to "Enable Partial Results Stabilization",
        "default" to "-- DEFAULT (HIGH) --",
        "low" to "Low",
        "medium" to "Medium",
        "high" to "High"
    ).map {
        SpinnerItem(it.key, it.value)
    }

    private val transcribePiiOptions: List<SpinnerItem> = mapOf<String, String>(
        "" to "ALL",
        "BANK_ROUTING" to "BANK ROUTING",
        "CREDIT_DEBIT_NUMBER" to "CREDIT/DEBIT NUMBER",
        "CREDIT_DEBIT_CVV" to "CREDIT/DEBIT CVV",
        "CREDIT_DEBIT_EXPIRY" to "CREDIT/DEBIT EXPIRY",
        "PIN" to "PIN",
        "EMAIL" to "EMAIL",
        "ADDRESS" to "ADDRESS",
        "NAME" to "NAME",
        "PHONE" to "PHONE NUMBER",
        "SSN" to "SSN"
    ).map {
        SpinnerItem(it.key, it.value)
    }

    private val transcribePreferredLanguageDefaultOption: List<SpinnerItem> = mapOf<String, String>(
        "" to "[Optional] Select preferred language"
    ).map {
        SpinnerItem(it.key, it.value)
    }

    private val transcribeIdentificationOptions: MutableList<SpinnerItem> = mutableListOf<SpinnerItem>()
    private val transcribeRedactionOptions: MutableList<SpinnerItem> = mutableListOf<SpinnerItem>()
    private val transcribePartialResultStabilizationOptions: MutableList<SpinnerItem> = transcribePartialResultStabilizationValues.toMutableList()
    private val preferredLanguageOptions: MutableList<SpinnerItem> = mutableListOf<SpinnerItem>()
    private val languageOptionsSelected: MutableSet<TranscribeLanguageOption> = mutableSetOf<TranscribeLanguageOption>()
    private val languageOptions: HashMap<String, MutableList<SpinnerItem?>> = HashMap()

    private val transcribeMedicalLanguages: List<SpinnerItem> =
        arrayOf("en-US").mapNotNull { l ->
            languagesMap[l]?.let {
                SpinnerItem(l, it)
        }
    }

    private val transcribeMedicalRegions: List<SpinnerItem> = arrayOf(
        "auto",
        "",
        "ap-southeast-2",
        "ca-central-1",
        "eu-west-1",
        "us-east-1",
        "us-east-2",
        "us-west-2"
    ).mapNotNull { l ->
        regionsMap[l]?.let {
            SpinnerItem(l, it)
        }
    }

    private val transcribeEngines: List<SpinnerItem> = mapOf<String, String>(
        "transcribe" to "Amazon Transcribe",
        "transcribe_medical" to "Amazon Transcribe Medical"
    ).map {
        SpinnerItem(it.key, it.value)
    }

    private lateinit var listener: TranscriptionConfigurationEventListener

    private val TAG = "TranscriptionConfigurationFragment"

    private lateinit var transcribeEngineSpinner: Spinner
    private lateinit var transcribeEngineAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var languageSpinner: Spinner
    private lateinit var languageAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var regionSpinner: Spinner
    private lateinit var regionAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var partialResultsStabilizationSpinner: Spinner
    private lateinit var partialResultsStabilizationAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var piiIdentificationSpinner: Spinner
    private lateinit var piiIdentificationAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var piiRedactionSpinner: Spinner
    private lateinit var piiRedactionAdapter: ArrayAdapter<SpinnerItem>
    private lateinit var phiIdentificationCheckBox: CheckBox
    private lateinit var customLanguageModelCheckbox: CheckBox
    private lateinit var customLanguageModelEditText: EditText
    private lateinit var identifyLanguageCheckbox: CheckBox
    private lateinit var languageOptionsListView: ExpandableListView
    private lateinit var languageOptionsTextView: TextView
    private lateinit var languageOptionsAdapter: ExpandableListAdapter
    private lateinit var preferredLanguageSpinner: Spinner
    private lateinit var preferredLanguageAdapter: ArrayAdapter<SpinnerItem>

    private val TRANSCRIBE_ENGINE_SPINNER_INDEX_KEY = "transcribeEngineSpinnerIndex"
    private val LANGUAGE_SPINNER_INDEX_KEY = "languageSpinnerIndex"
    private val REGION_SPINNER_INDEX_KEY = "regionSpinnerIndex"
    private val PARTIAL_RESULTS_STABILIZATION_INDEX_KEY = "partialResultsStabilizationSpinnerIndex"
    private val PII_CONTENT_IDENTIFICATION_SPINNER_INDEX_KEY = "piiContentIdentificationSpinnerIndex"
    private val PII_CONTENT_REDACTION_SPINNER_INDEX_KEY = "piiContentRedactionSpinnerIndex"
    private val PHI_CONTENT_IDENTIFICATION_ENABLED_KEY = "phiContentIdentificationEnabled"
    private val CUSTOM_LANGUAGE_MODEL_NAME_KEY = "customLanguageModelName"
    private val IDENTIFY_LANGUAGE_ENABLED_KEY = "identifyLanguageEnabled"
    private val PREFERRED_LANGUAGE_INDEX_KEY = "preferredLanguageIndex"

    companion object {
        fun newInstance(meetingId: String): TranscriptionConfigFragment {
            val fragment = TranscriptionConfigFragment()

            fragment.arguments =
                Bundle().apply {
                    putString(HomeActivity.MEETING_ID_KEY, meetingId)
                }
            return fragment
        }
    }

    interface TranscriptionConfigurationEventListener {
        fun onStartTranscription(
            engine: SpinnerItem,
            language: SpinnerItem?,
            region: SpinnerItem,
            partialResultsStability: SpinnerItem,
            contentIdentificationType: SpinnerItem?,
            contentRedactionType: SpinnerItem?,
            languageModelName: String?,
            identifyLanguage: Boolean,
            languageOptions: String?,
            preferredLanguage: SpinnerItem?
        )
    }

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

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

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

        view.findViewById<Button>(R.id.buttonStartTranscription)?.setOnClickListener {
            listener.onStartTranscription(
                transcribeEngineSpinner.selectedItem as SpinnerItem,
                languageSpinner.selectedItem?.let {
                    languageSpinner.selectedItem as SpinnerItem },
                regionSpinner.selectedItem as SpinnerItem,
                partialResultsStabilizationSpinner.selectedItem as SpinnerItem,
                if (piiIdentificationSpinner.isEnabled) {
                    piiIdentificationSpinner.selectedItem as SpinnerItem } else null,
                if (piiRedactionSpinner.isEnabled) {
                    piiRedactionSpinner.selectedItem as SpinnerItem } else null,
                if (customLanguageModelCheckbox.isEnabled) {
                    customLanguageModelEditText.text.toString() } else null,
                identifyLanguageCheckbox.isChecked,
                if (identifyLanguageCheckbox.isChecked) {
                    formatLanguageOptions() } else null,
                if (identifyLanguageCheckbox.isChecked) {
                    preferredLanguageSpinner.selectedItem?.let {
                        preferredLanguageSpinner.selectedItem as SpinnerItem } } else null
            )
        }

        // 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.

        transcribeEngineSpinner = view.findViewById(R.id.spinnerTranscribeEngine)
        transcribeEngineAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, transcribeEngines)
        transcribeEngineAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        transcribeEngineSpinner.adapter = transcribeEngineAdapter
        transcribeEngineSpinner.isSelected = false
        transcribeEngineSpinner.setSelection(0, true)
        transcribeEngineSpinner.onItemSelectedListener = onTranscribeEngineSelected

        languageSpinner = view.findViewById(R.id.spinnerLanguage)
        languageAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, languages)
        languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        languageSpinner.adapter = languageAdapter
        languageSpinner.isSelected = false

        regionSpinner = view.findViewById(R.id.spinnerRegion)
        regionAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, regions)
        regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        regionSpinner.adapter = regionAdapter
        regionSpinner.isSelected = false

        identifyLanguageCheckbox = view.findViewById(R.id.checkboxIdentifyLanguage)
        languageOptionsTextView = view.findViewById(R.id.textViewLanguageOptions)
        identifyLanguageCheckbox.setOnCheckedChangeListener { languageOptionDialogBox, isChecked ->
            if (isChecked) {
                showLanguageOptionsAlertDialog(languageOptionDialogBox)
                languageSpinner.isEnabled = false
                piiIdentificationSpinner.isEnabled = false
                piiRedactionSpinner.isEnabled = false
                phiIdentificationCheckBox.isEnabled = false
                preferredLanguageSpinner.visibility = View.VISIBLE
                languageOptionsTextView.visibility = View.VISIBLE
                customLanguageModelCheckbox.isEnabled = false
                customLanguageModelEditText.isEnabled = false
                customLanguageModelEditText.visibility = View.GONE
            } else {
                preferredLanguageSpinner.visibility = View.GONE
                languageOptionsTextView.isEnabled = false
                languageSpinner.isEnabled = true
                piiIdentificationSpinner.isEnabled = true
                piiRedactionSpinner.isEnabled = true
                languageOptionsTextView.visibility = View.GONE
                customLanguageModelCheckbox.isEnabled = true
                customLanguageModelEditText.isEnabled = true
                customLanguageModelEditText.visibility = View.VISIBLE
            }
        }

        preferredLanguageSpinner = view.findViewById(R.id.spinnerPreferredLanguage)
        preferredLanguageAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, preferredLanguageOptions)
        preferredLanguageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        preferredLanguageSpinner.adapter = preferredLanguageAdapter
        preferredLanguageSpinner.isSelected = false

        partialResultsStabilizationSpinner = view.findViewById(R.id.spinnerPartialResultsStabilization)
        partialResultsStabilizationAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, transcribePartialResultStabilizationOptions)
        partialResultsStabilizationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        partialResultsStabilizationSpinner.adapter = partialResultsStabilizationAdapter
        partialResultsStabilizationSpinner.isSelected = false
        partialResultsStabilizationSpinner.setSelection(0, true)

        piiIdentificationSpinner = view.findViewById(R.id.spinnerPIIContentIdentification)
        piiIdentificationAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, transcribeIdentificationOptions)
        piiIdentificationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        piiIdentificationSpinner.adapter = piiIdentificationAdapter
        piiIdentificationSpinner.isSelected = false
        piiIdentificationSpinner.setSelection(0, true)
        piiIdentificationSpinner.onItemSelectedListener = onPIIContentIdentificationSelected

        piiRedactionSpinner = view.findViewById(R.id.spinnerPIIContentRedaction)
        piiRedactionAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, transcribeRedactionOptions)
        piiRedactionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        piiRedactionSpinner.adapter = piiRedactionAdapter
        piiRedactionSpinner.isSelected = false
        piiRedactionSpinner.setSelection(0, true)
        piiRedactionSpinner.onItemSelectedListener = onPIIContentRedactionSelected

        phiIdentificationCheckBox = view.findViewById(R.id.checkboxPHIContentIdentification)
        phiIdentificationCheckBox.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked) {
                piiIdentificationSpinner.setSelection(1, true)
            } else {
                piiIdentificationSpinner.setSelection(0, true)
            }
        }

        customLanguageModelEditText = view.findViewById(R.id.editTextCustomLanguageModel)

        customLanguageModelCheckbox = view.findViewById(R.id.checkboxCustomLanguageModel)
        // Show / Hide EditText field for custom language model when checkbox clicked.
        customLanguageModelCheckbox.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked) {
                customLanguageModelEditText.visibility = View.VISIBLE
            } else {
                customLanguageModelEditText.visibility = View.GONE
            }
        }

        uiScope.launch {
            populateLanguages(transcribeLanguages, languages, languageAdapter)
            populateRegions(transcribeRegions, regions, regionAdapter)
            populateTranscriptionOptions(transcribePiiOptions, transcribeIdentificationOptions, piiIdentificationAdapter, "Identification")
            populateTranscriptionOptions(transcribePiiOptions, transcribeRedactionOptions, piiRedactionAdapter, "Redaction")
            populateLanguageOptions(languageOptions)

            var transcribeEngineSpinnerIndex = 0
            var languageSpinnerIndex = 0
            var regionSpinnerIndex = 0
            var partialResultsStabilizationSpinnerIndex = 0
            var piiContentIdentificationSpinnerIndex = 0
            var piiContentRedactionSpinnerIndex = 0
            var phiContentIdentificationEnabled = false
            var customLanguageModelName = resources.getString(R.string.custom_language_checkbox)
            var preferredLanguageSpinnerIndex = 0
            var identifyLanguageEnabled = false
            if (savedInstanceState != null) {
                transcribeEngineSpinnerIndex = savedInstanceState.getInt(TRANSCRIBE_ENGINE_SPINNER_INDEX_KEY, 0)
                languageSpinnerIndex = savedInstanceState.getInt(LANGUAGE_SPINNER_INDEX_KEY, 0)
                regionSpinnerIndex = savedInstanceState.getInt(REGION_SPINNER_INDEX_KEY, 0)
                partialResultsStabilizationSpinnerIndex = savedInstanceState.getInt(PARTIAL_RESULTS_STABILIZATION_INDEX_KEY, 0)
                piiContentIdentificationSpinnerIndex = savedInstanceState.getInt(PII_CONTENT_IDENTIFICATION_SPINNER_INDEX_KEY, 0)
                piiContentRedactionSpinnerIndex = savedInstanceState.getInt(PII_CONTENT_REDACTION_SPINNER_INDEX_KEY, 0)
                phiContentIdentificationEnabled = savedInstanceState.getBoolean(PHI_CONTENT_IDENTIFICATION_ENABLED_KEY, false)
                customLanguageModelName = savedInstanceState.getString(CUSTOM_LANGUAGE_MODEL_NAME_KEY, resources.getString(R.string.custom_language_checkbox))
                identifyLanguageEnabled = savedInstanceState.getBoolean(IDENTIFY_LANGUAGE_ENABLED_KEY, false)
                preferredLanguageSpinnerIndex = savedInstanceState.getInt(PREFERRED_LANGUAGE_INDEX_KEY, 0)
            }

            transcribeEngineSpinner.setSelection(transcribeEngineSpinnerIndex)
            languageSpinner.setSelection(languageSpinnerIndex)
            regionSpinner.setSelection(regionSpinnerIndex)
            partialResultsStabilizationSpinner.setSelection(partialResultsStabilizationSpinnerIndex)
            piiIdentificationSpinner.setSelection(piiContentIdentificationSpinnerIndex)
            piiRedactionSpinner.setSelection(piiContentRedactionSpinnerIndex)
            checkboxPHIContentIdentification.isChecked = phiContentIdentificationEnabled
            checkboxCustomLanguageModel.isChecked = customLanguageModelName != (resources.getString(R.string.custom_language_checkbox))
            checkboxCustomLanguageModel.text = customLanguageModelName
            identifyLanguageCheckbox.isChecked = identifyLanguageEnabled
            preferredLanguageSpinner.setSelection(preferredLanguageSpinnerIndex)
        }

        return view
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(TRANSCRIBE_ENGINE_SPINNER_INDEX_KEY, transcribeEngineSpinner.selectedItemPosition)
        outState.putInt(LANGUAGE_SPINNER_INDEX_KEY, languageSpinner.selectedItemPosition)
        outState.putInt(REGION_SPINNER_INDEX_KEY, regionSpinner.selectedItemPosition)
        outState.putInt(PARTIAL_RESULTS_STABILIZATION_INDEX_KEY, partialResultsStabilizationSpinner.selectedItemPosition)
        outState.putInt(PII_CONTENT_IDENTIFICATION_SPINNER_INDEX_KEY, piiIdentificationSpinner.selectedItemPosition)
        outState.putInt(PII_CONTENT_REDACTION_SPINNER_INDEX_KEY, piiRedactionSpinner.selectedItemPosition)
        outState.putBoolean(PHI_CONTENT_IDENTIFICATION_ENABLED_KEY, phiIdentificationCheckBox.isChecked)
        outState.putString(CUSTOM_LANGUAGE_MODEL_NAME_KEY, checkboxCustomLanguageModel.text.toString())
        outState.putInt(PREFERRED_LANGUAGE_INDEX_KEY, preferredLanguageSpinner.selectedItemPosition)
        outState.putBoolean(IDENTIFY_LANGUAGE_ENABLED_KEY, identifyLanguageCheckbox.isChecked)
    }

    private val onTranscribeEngineSelected = object : OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            if (position < transcribeEngines.size) {
                populateLanguages(transcribeMedicalLanguages, languages, languageAdapter)
                populateRegions(transcribeMedicalRegions, regions, regionAdapter)
                when (transcribeEngines[position].spinnerText) {
                    "transcribe_medical" -> {
                        displayAdditionalTranscriptionOptions(true)
                    }
                    "transcribe" -> {
                        displayAdditionalTranscriptionOptions(false)
                    }
                    else -> {
                        logger.error(TAG, "Invalid in TranscribeEngine selected")
                    }
                }
            } else {
                logger.error(TAG, "Incorrect position in TranscribeEngine spinner")
            }
            resetSpinner()
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
        }
    }

    // Reset the redaction spinner when content identification selected.
    private val onPIIContentIdentificationSelected = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            if (position < transcribeIdentificationOptions.size && position > 0) {
                populateTranscriptionOptions(transcribePiiOptions, transcribeRedactionOptions, piiRedactionAdapter, "Redaction")
                piiRedactionSpinner.setSelection(0, true)
            } else {
                logger.error(TAG, "Incorrect position in TranscribeIdentification spinner")
            }
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
        }
    }

    // Reset the identification spinner when content redaction selected.
    private val onPIIContentRedactionSelected = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            if (position < transcribeRedactionOptions.size && position > 0) {
                populateTranscriptionOptions(transcribePiiOptions, transcribeIdentificationOptions, piiIdentificationAdapter, "Identification")
                piiIdentificationSpinner.setSelection(0, true)
            } else {
                logger.error(TAG, "Incorrect position in TranscribeRedaction spinner")
            }
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
        }
    }

    private fun populateLanguages(newList: List<SpinnerItem>, currentList: MutableList<SpinnerItem>, adapter: ArrayAdapter<SpinnerItem>) {
        currentList.clear()
        currentList.addAll(newList)
        adapter.notifyDataSetChanged()
    }

    private fun populateRegions(newList: List<SpinnerItem>, currentList: MutableList<SpinnerItem>, adapter: ArrayAdapter<SpinnerItem>) {
        currentList.clear()
        currentList.addAll(newList)
        adapter.notifyDataSetChanged()
    }

    // Populate content identification / redaction spinners and set the label of the spinner with the given 'type' (E.G identification or redaction).
    private fun populateTranscriptionOptions(newList: List<SpinnerItem>, currentList: MutableList<SpinnerItem>, adapter: ArrayAdapter<SpinnerItem>, type: String) {
        currentList.clear()
        currentList.addAll(newList)
        currentList.add(0, SpinnerItem(null, "Enable PII Content $type"))
        adapter.notifyDataSetChanged()
    }

    private fun populateLanguageOptions(languageOptions: HashMap<String, MutableList<SpinnerItem?>>) {
        for ((locale, variant) in languageGroupMapping) {
            if (languageOptions.containsKey(locale)) {
                languageOptions[locale]?.add(languagesMap[variant]?.let {
                    SpinnerItem(variant, it) })
            } else {
                languageOptions[locale] =
                    mutableListOf(languagesMap[variant]?.let { SpinnerItem(variant, it) })
            }
        }
    }

    private fun populatePreferredLanguage(newList: List<SpinnerItem>, currentList: MutableList<SpinnerItem>, adapter: ArrayAdapter<SpinnerItem>) {
        currentList.clear()
        currentList.add(transcribePreferredLanguageDefaultOption[0])
        currentList.addAll(newList)
        adapter.notifyDataSetChanged()
    }

    private fun resetSpinner() {
        languageSpinner.setSelection(0, true)
        regionSpinner.setSelection(0, true)
    }

    // Enable / Disable additional transcription options based on engine selected.
    private fun displayAdditionalTranscriptionOptions(isTranscribeMedical: Boolean) {
        identifyLanguageCheckbox.isChecked = false
        piiIdentificationSpinner.setSelection(0, true)
        piiRedactionSpinner.setSelection(0, true)
        partialResultsStabilizationSpinner.setSelection(0, true)
        checkboxCustomLanguageModel.isChecked = false
        phiIdentificationCheckBox.isChecked = false
        piiIdentificationSpinner.setSelection(0, true)
        checkboxCustomLanguageModel.text = resources.getString(R.string.custom_language_checkbox)
        customLanguageModelEditText.visibility = View.GONE

        piiRedactionSpinner.visibility = if (isTranscribeMedical) View.GONE else View.VISIBLE
        piiIdentificationSpinner.visibility = if (isTranscribeMedical) View.GONE else View.VISIBLE
        checkboxCustomLanguageModel.visibility = if (isTranscribeMedical) View.GONE else View.VISIBLE
        partialResultsStabilizationSpinner.visibility = if (isTranscribeMedical) View.GONE else View.VISIBLE
        phiIdentificationCheckBox.visibility = if (isTranscribeMedical) View.VISIBLE else View.GONE
        identifyLanguageCheckbox.visibility = if (isTranscribeMedical) View.GONE else View.VISIBLE
        languageSpinner.isEnabled = isTranscribeMedical
        phiIdentificationCheckBox.isEnabled = true
        if (isTranscribeMedical) {
            languageOptionsTextView.visibility = View.GONE
            preferredLanguageSpinner.visibility = View.GONE
        }
    }

    private fun showLanguageOptionsAlertDialog(languageOptionDialogBox: CompoundButton) {
        val builder = AlertDialog.Builder(this.context)
        val languageOptionsAlertDialog =
            layoutInflater.inflate(R.layout.alert_dialog_language_options, null)
        languageOptionsListView = languageOptionsAlertDialog.findViewById<ExpandableListView>(R.id.expandableListViewLanguageOptions)

        languageOptionsAdapter = LanguageOptionsAdapter(this.requireContext(), languageOptions, languageGroups, languageOptionsSelected)
        languageOptionsListView.setAdapter(languageOptionsAdapter)
        languageOptionsListView.setOnChildClickListener(OnChildClickListener { _, view, groupPosition, childPosition, _ ->

            val languageOptionCheck: CheckedTextView = view.findViewById(R.id.checkedTextViewLanguageOptionRow)
            languageOptionCheck.toggle()
            val languageSelected: SpinnerItem? = languageOptions[languageGroups[groupPosition]]?.get(childPosition)
            val selectedCell: TranscribeLanguageOption? =
                languageSelected?.let { TranscribeLanguageOption(groupPosition, childPosition, it) }
            selectedCell?.let {
                if (languageOptionsSelected.contains(selectedCell)) {
                    languageOptionsSelected.remove(selectedCell)
                } else {
                    languageOptionsSelected.add(selectedCell)
                }
            }

            validateLanguageOptions(
                languageGroups,
                languageOptionsSelected,
                languageOptionsAlertDialog
            )

            return@OnChildClickListener false
        })

        expandLanguageOptionsGroup()

        builder.setView(languageOptionsAlertDialog)
        val alertDialog = builder.create()
        alertDialog.show()

        val saveButton: Button = languageOptionsAlertDialog.findViewById(R.id.buttonSaveLanguageOptions)
        saveButton.setOnClickListener {
            if (validateLanguageOptions(languageGroups, languageOptionsSelected, languageOptionsAlertDialog)) {
                val languageOptionsSelected: List<SpinnerItem> =
                    languageOptionsSelected.map { languageOptionSelected -> languageOptionSelected.transcribeLanguage }
                populatePreferredLanguage(languageOptionsSelected, preferredLanguageOptions, preferredLanguageAdapter)
                languageOptionsTextView.text = String.format("%s %s", getString(R.string.language_options_text_title),
                    languageOptionsSelected.joinToString(", "))

                alertDialog.dismiss()
            }
        }

        val cancelButton: Button = languageOptionsAlertDialog.findViewById(R.id.buttonCancelLanguageOptions)
        cancelButton.setOnClickListener {
            languageOptionDialogBox.isChecked = false
            alertDialog.dismiss()
        }
    }

    private fun expandLanguageOptionsGroup() {
        for (i in 0 until languageOptionsAdapter.groupCount) {
            languageOptionsListView.expandGroup(i)
        }
    }

    private fun formatLanguageOptions(): String {
        return if (languageOptionsSelected.isEmpty()) "" else
            languageOptionsSelected.joinToString(",") {
                    languageOptionSelected -> languageOptionSelected.transcribeLanguage.spinnerText.toString()
            }
    }

    private fun validateLanguageOptions(
        languageLocales: ArrayList<String>,
        languageOptionsSelectedSet: MutableSet<TranscribeLanguageOption>,
        view: View
    ): Boolean {
        var isValid = false
        val duplicateLanguageLocale: HashSet<String> = HashSet<String>()
        val languageLocaleSelectedSet: HashSet<Int> = HashSet<Int>()
        for (languageOptionSelected in languageOptionsSelectedSet) {
            if (languageLocaleSelectedSet.contains(languageOptionSelected.languageGroupIndex) &&
                !duplicateLanguageLocale.contains(languageLocales[languageOptionSelected.languageGroupIndex])) {
                duplicateLanguageLocale.add(languageLocales[languageOptionSelected.languageGroupIndex])
            } else {
                languageLocaleSelectedSet.add(languageOptionSelected.languageGroupIndex)
            }
        }

        val displayErrorMessageText: String = when {
            duplicateLanguageLocale.size > 0 -> {
                getString(R.string.user_notification_language_option_invalid_selection,
                    duplicateLanguageLocale.joinToString(", "))
            }
            languageLocaleSelectedSet.size < 2 -> {
                getString(R.string.user_notification_language_option_missing_selection)
            }
            else -> {
                isValid = true
                ""
            }
        }

        view.findViewById<TextView>(R.id.textViewErrorLanguageOptions).text = displayErrorMessageText
        return isValid
    }
}


package com.amazonaws.services.chime.sdkdemo.model

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class DebugSettingsViewModel : ViewModel() {
    val endpointUrl = MutableLiveData("")
    val primaryMeetingId = MutableLiveData("")

    fun sendEndpointUrl(data: String) {
        endpointUrl.value = data
    }

    fun sendPrimaryMeetingId(data: String) {
        primaryMeetingId.value = data
    }
}


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

package com.amazonaws.services.chime.sdkdemo.model

import androidx.lifecycle.ViewModel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
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.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdkdemo.data.Caption
import com.amazonaws.services.chime.sdkdemo.data.Message
import com.amazonaws.services.chime.sdkdemo.data.MetricData
import com.amazonaws.services.chime.sdkdemo.data.RosterAttendee
import com.amazonaws.services.chime.sdkdemo.data.VideoCollectionTile
import com.amazonaws.services.chime.sdkdemo.fragment.MeetingFragment
import com.amazonaws.services.chime.sdkdemo.utils.isContentShare
import kotlin.math.ceil
import kotlin.math.min

// This will be used for keeping state after rotation
class MeetingModel : ViewModel() {
    val localTileId = 0
    private val videoTileCountPerPage = 4

    val currentMetrics = mutableMapOf<String, MetricData>()
    val currentRoster = mutableMapOf<String, RosterAttendee>()
    var localVideoTileState: VideoCollectionTile? = null
    // Make it private to prevent consumers from manipulating it directly
    // In order to access it, use getRemoteVideoTileStates()
    private val remoteVideoTileStates = mutableListOf<VideoCollectionTile>()

    // Make it private to prevent consumers from manipulating it directly
    // In order to access it, use getRemoteVideoSourceConfigurations()
    private val remoteVideoSourceConfigurations =
        mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()

    private val contentShareRemoteVideoSourceConfigurations =
        mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()

    // Video sources without matching video tile state, they will be moved to
    // remoteVideoSourceConfigurations once the matching video tile state is added
    private val pendingVideoSourceConfigurations =
        mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()

    private val videoSourcesToBeSubscribed =
        mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()

    private val videoSourcesToBeUnsubscribed = mutableSetOf<RemoteVideoSource>()

    // contains local video tile
    val videoStatesInCurrentPage = mutableListOf<VideoCollectionTile>()
    val userPausedVideoTileIds = mutableSetOf<Int>()
    val currentScreenTiles = mutableListOf<VideoCollectionTile>()
    var currentVideoPageIndex = 0
    var currentMediaDevices = listOf<MediaDevice>()
    var currentMessages = mutableListOf<Message>()
    val currentCaptions = mutableListOf<Caption>()
    val currentCaptionIndices = mutableMapOf<String, Int>()

    var isMuted = false
    var isCameraOn = false
    var isDeviceListDialogOn = false
    var isAdditionalOptionsDialogOn = false
    var isSharingContent = false
    var isLiveTranscriptionEnabled = false
    var lastReceivedMessageTimestamp = 0L
    var tabIndex = 0
    var isUsingCameraCaptureSource = true
    var isLocalVideoStarted = false
    var wasLocalVideoStarted = false
    var isUsingGpuVideoProcessor = false
    var isUsingCpuVideoProcessor = false
    var isUsingBackgroundBlur = false
    var isUsingBackgroundReplacement = false
    var localVideoMaxBitRateKbps = 0
    var isCameraSendAvailable = false

    fun getRemoteVideoTileStates(): List<VideoCollectionTile> {
        return remoteVideoTileStates
    }

    // Add VideoTileState to remoteVideoTileStates, if there is matching video source
    // in pendingVideoSourceConfigurations, move it to remoteVideoSourceConfigurations
    fun addRemoteVideoTileState(state: VideoCollectionTile) {
        remoteVideoTileStates.add(state)

        // Move matching RemoteVideSource from pendingVideoSourceConfigurations
        // to remoteVideoSourceConfigurations
        val attendeeId = state.videoTileState.attendeeId
        val movingVideoSources = pendingVideoSourceConfigurations
            .filter { it.key.attendeeId == attendeeId }
        movingVideoSources.forEach { pendingVideoSourceConfigurations.remove(it.key) }
        remoteVideoSourceConfigurations.putAll(movingVideoSources)
    }

    fun removeRemoteVideoTileState(tileId: Int) {
        remoteVideoTileStates.removeAll { it.videoTileState.tileId == tileId }
    }

    fun updateVideoStatesInCurrentPage() {
        videoStatesInCurrentPage.clear()

        if (localVideoTileState != null) {
            videoStatesInCurrentPage.add(localVideoTileState!!)
        }
        val remoteVideoTileCountPerPage =
            if (localVideoTileState == null) videoTileCountPerPage else (videoTileCountPerPage - 1)
        val remoteVideoStartIndex = currentVideoPageIndex * remoteVideoTileCountPerPage
        val remoteVideoEndIndex =
            min(remoteVideoTileStates.size, remoteVideoStartIndex + remoteVideoTileCountPerPage) - 1
        if (remoteVideoStartIndex <= remoteVideoEndIndex) {
            videoStatesInCurrentPage.addAll(remoteVideoTileStates.slice(remoteVideoStartIndex..remoteVideoEndIndex))
        }
    }

    fun getRemoteVideoSourceConfigurations(): Map<RemoteVideoSource, VideoSubscriptionConfiguration> {
        return remoteVideoSourceConfigurations
    }

    fun setRemoteVideoSourceConfigurations(source: RemoteVideoSource, config: VideoSubscriptionConfiguration) {
        remoteVideoSourceConfigurations[source] = config
    }

    fun updateRemoteVideoStatesBasedOnActiveSpeakers(activeSpeakers: Array<AttendeeInfo>) {
        val activeSpeakerIds = activeSpeakers.map { it.attendeeId }.toHashSet()
        remoteVideoTileStates.sortWith(Comparator<VideoCollectionTile> { lhs, rhs ->
            val lhsIsActiveSpeaker = activeSpeakerIds.contains(lhs.videoTileState.attendeeId)
            val rhsIsActiveSpeaker = activeSpeakerIds.contains(rhs.videoTileState.attendeeId)

            when {
                lhsIsActiveSpeaker && !rhsIsActiveSpeaker -> -1
                !lhsIsActiveSpeaker && rhsIsActiveSpeaker -> 1
                else -> 0
            }
        })
    }

    fun remoteVideoCountInCurrentPage(): Int {
        return videoStatesInCurrentPage.filter { !it.videoTileState.isLocalTile }.size
    }

    fun canGoToPrevVideoPage(): Boolean {
        return currentVideoPageIndex > 0
    }

    fun canGoToNextVideoPage(): Boolean {
        val remoteVideoTileCountPerPage =
            if (localVideoTileState == null) videoTileCountPerPage else (videoTileCountPerPage - 1)
        val maxVideoPageIndex =
            ceil(remoteVideoTileStates.size.toDouble() / remoteVideoTileCountPerPage).toInt() - 1
        return currentVideoPageIndex < maxVideoPageIndex
    }

    fun addVideoSource(source: RemoteVideoSource, config: VideoSubscriptionConfiguration) {
        if (source.isContentShare()) {
            contentShareRemoteVideoSourceConfigurations[source] = config
        } else {
            if (remoteVideoSourceConfigurations[source] == null) {
                pendingVideoSourceConfigurations[source] = config
            } else {
                remoteVideoSourceConfigurations[source] = config
            }
        }
    }

    fun removeVideoSource(source: RemoteVideoSource) {
        remoteVideoSourceConfigurations.remove(source)
        pendingVideoSourceConfigurations.remove(source)
        contentShareRemoteVideoSourceConfigurations.remove(source)
    }

    // A helper function for retrieving remote video sources in current page. It will retrieve
    // video sources in remoteVideoSourceConfigurations based on videoStatesInCurrentPage, if
    // videoStatesInCurrentPage.count() is smaller than videoTileCountPerPage and
    // pendingVideoSourceConfigurations is not empty, it will retrieve
    // (videoTileCountPerPage - videoStatesInCurrentPage.size) items from
    // pendingVideoSourceConfigurations, and add to result
    fun getRemoteVideoSourcesInCurrentPage(): Map<RemoteVideoSource, VideoSubscriptionConfiguration> {
        val result = mutableMapOf<RemoteVideoSource, VideoSubscriptionConfiguration>()
        // remote video sources in current page
        result.putAll(getRemoteVideoSources(videoStatesInCurrentPage))

        var numVideoSourcesNeededFromPending = videoTileCountPerPage - videoStatesInCurrentPage.size
        for (pendingVideoSourceEntry in pendingVideoSourceConfigurations.entries.iterator()) {
            if (numVideoSourcesNeededFromPending == 0) {
                break
            }
            result[pendingVideoSourceEntry.key] = pendingVideoSourceEntry.value
            numVideoSourcesNeededFromPending--
        }
        return result
    }

    // A helper function for retrieving all remote video sources from
    // remoteVideoSourceConfigurations which are not in current page
    private fun remoteVideoSourcesNotInCurrentPage(): Set<RemoteVideoSource> {
        val keysToRemove = getRemoteVideoSourcesInCurrentPage().keys
        return remoteVideoSourceConfigurations.minus(keysToRemove).keys
    }

    // Based on tabIndex, calculate the videos sources need to be:
    //  - added/updated to subscription
    //  - removed from subscription
    // The results will be added to videoSourcesToBeSubscribed and videoSourcesToBeUnsubscribed
    // respectively
    fun updateRemoteVideoSourceSelection() {
        when (tabIndex) {
            MeetingFragment.SubTab.Video.position -> {
                // If showing video screen, only subscribe the video sources on current video
                // page index, unsubscribe the rest
                videoSourcesToBeSubscribed.putAll(getRemoteVideoSourcesInCurrentPage())
                videoSourcesToBeUnsubscribed.addAll(remoteVideoSourcesNotInCurrentPage())
                videoSourcesToBeUnsubscribed.addAll(contentShareRemoteVideoSourceConfigurations.keys)
            }
            MeetingFragment.SubTab.Screen.position -> {
                videoSourcesToBeSubscribed.putAll(contentShareRemoteVideoSourceConfigurations)
                videoSourcesToBeUnsubscribed.addAll(remoteVideoSourceConfigurations.keys)
            }
            else -> {
                videoSourcesToBeUnsubscribed.addAll(remoteVideoSourceConfigurations.keys)
                videoSourcesToBeUnsubscribed.addAll(contentShareRemoteVideoSourceConfigurations.keys)
            }
        }
    }

    // Update video source subscription. `audioVideo` has to be passed from MeetingFragment
    // into this function as a parameter for now, it should be moved to MeetingModel in the future
    // for practicing MVVM pattern
    fun updateRemoteVideoSourceSubscription(audioVideo: AudioVideoFacade) {
        if (videoSourcesToBeSubscribed.isEmpty() && videoSourcesToBeUnsubscribed.isEmpty()) {
            return
        }
        audioVideo.updateVideoSourceSubscriptions(videoSourcesToBeSubscribed,
            videoSourcesToBeUnsubscribed.toTypedArray())
        videoSourcesToBeSubscribed.clear()
        videoSourcesToBeUnsubscribed.clear()
    }

    // A helper function for fetching remote video sources/config from
    // remoteVideoSourceConfigurations based on attendee ID, given a list of video tiles,
    private fun getRemoteVideoSources(videoTiles: List<VideoCollectionTile>):
        Map<RemoteVideoSource, VideoSubscriptionConfiguration> {
        val attendeeIds = videoTiles.map { it.videoTileState.attendeeId }.toHashSet()
        return remoteVideoSourceConfigurations.filter { attendeeIds.contains(it.key.attendeeId) }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.model

import androidx.lifecycle.ViewModel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
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.gl.DefaultEglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSession
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
import com.amazonaws.services.chime.sdkdemo.device.ScreenShareManager
import com.amazonaws.services.chime.sdkdemo.utils.CpuVideoProcessor
import com.amazonaws.services.chime.sdkdemo.utils.GpuVideoProcessor

class MeetingSessionModel : ViewModel() {
    lateinit var meetingSession: MeetingSession

    val credentials: MeetingSessionCredentials
        get() = meetingSession.configuration.credentials

    val configuration: MeetingSessionConfiguration
        get() = meetingSession.configuration

    val audioVideo: AudioVideoFacade
        get() = meetingSession.audioVideo

    // Graphics/capture related objects
    val eglCoreFactory: EglCoreFactory = DefaultEglCoreFactory()
    lateinit var cameraCaptureSource: CameraCaptureSource
    lateinit var gpuVideoProcessor: GpuVideoProcessor
    lateinit var cpuVideoProcessor: CpuVideoProcessor
    lateinit var backgroundBlurVideoFrameProcessor: BackgroundBlurVideoFrameProcessor
    lateinit var backgroundReplacementVideoFrameProcessor: BackgroundReplacementVideoFrameProcessor

    // Source for screen capture and share, will be set only if created in call
    var screenShareManager: ScreenShareManager? = null

    // For use with replica promotions, null if not a replica meeting
    var primaryExternalMeetingId: String? = null
}


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

package com.amazonaws.services.chime.sdkdemo.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.amazonaws.services.chime.sdkdemo.R

class ScreenCaptureService : Service() {
    private lateinit var notificationManager: NotificationManager

    private val CHANNEL_ID = "ScreenCaptureServiceChannelID"
    private val CHANNEL_NAME = "Screen Share"
    private val SERVICE_ID = 1

    private val binder = ScreenCaptureBinder()

    inner class ScreenCaptureBinder : Binder() {
        fun getService(): ScreenCaptureService = this@ScreenCaptureService
    }

    override fun onCreate() {
        super.onCreate()

        notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }

        startForeground(
            SERVICE_ID,
            NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(getString(R.string.screen_capture_notification_tile))
                .setContentText(getText(R.string.screen_capture_notification_text))
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .build()
        )
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = binder
}


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

package com.amazonaws.services.chime.sdkdemo.utils

import android.graphics.Matrix
import android.opengl.GLES11Ext
import android.opengl.GLES20
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlVideoFrameDrawer
import java.security.InvalidParameterException
import kotlin.math.hypot
import kotlin.math.roundToInt

/**
 * [BlackAndWhiteGlVideoFrameDrawer] simply draws the frames as opaque quads onto the current surface with a black and white filter.
 * It only supports OES texture frames.
 */
class BlackAndWhiteGlVideoFrameDrawer : GlVideoFrameDrawer {
    private var program: Int = -1
    private var vertexPositionLocation = 0
    private var textureCoordinateLocation = 0
    private var textureMatrixLocation = 0

    // Variables to store post-transform render information
    private val renderMatrix = Matrix()
    private val renderDestinationPoints = FloatArray(6)
    private var renderWidth = 0
    private var renderHeight = 0

    override fun drawFrame(
        frame: VideoFrame,
        viewportX: Int,
        viewportY: Int,
        viewportWidth: Int,
        viewportHeight: Int,
        additionalRenderMatrix: Matrix?
    ) {
        val isTextureFrame = frame.buffer is VideoFrameTextureBuffer
        val textureBuffer = frame.buffer as VideoFrameTextureBuffer
        if (!isTextureFrame || textureBuffer.type != VideoFrameTextureBuffer.Type.TEXTURE_OES) {
            throw InvalidParameterException("Only OES texture frames are expected")
        }

        // Set up and account for transformation
        val width: Int = frame.getRotatedWidth()
        val height: Int = frame.getRotatedHeight()
        calculateTransformedRenderSize(width, height, additionalRenderMatrix)
        if (renderWidth <= 0 || renderHeight <= 0) {
            return
        }
        renderMatrix.reset()
        // Need to translate origin before rotating
        renderMatrix.preTranslate(0.5f, 0.5f)
        renderMatrix.preRotate(frame.rotation.degrees.toFloat())
        // Translate back
        renderMatrix.preTranslate(-0.5f, -0.5f)
        // Apply additional matrix
        additionalRenderMatrix?.let { renderMatrix.preConcat(it) }

        // Apply texture frame matrix
        val finalMatrix = Matrix(textureBuffer.transformMatrix)
        finalMatrix.preConcat(renderMatrix)
        val finalGlMatrix = GlUtil.convertToGlTransformMatrix(finalMatrix)

        prepareShader(finalGlMatrix)

        // Bind the texture.
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureBuffer.textureId)

        // Draw the texture.
        GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight)
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
        GlUtil.checkGlError("Failed to draw OES texture")

        // Reset texture back to default texture
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
    }

    // Calculate the frame size after |renderMatrix| is applied. Stores the output in member variables
    // |renderWidth| and |renderHeight| to avoid allocations since this function is called for every frame.
    private fun calculateTransformedRenderSize(frameWidth: Int, frameHeight: Int, renderMatrix: Matrix?) {
        if (renderMatrix == null) {
            renderWidth = frameWidth
            renderHeight = frameHeight
            return
        }
        // Transform the texture coordinates (in the range [0, 1]) according to |renderMatrix|.
        renderMatrix.mapPoints(renderDestinationPoints, srcPoints)

        // Multiply with the width and height to get the positions in terms of pixels.
        for (i in 0..2) {
            renderDestinationPoints[i * 2 + 0] = renderDestinationPoints[i * 2 + 0] * frameWidth
            renderDestinationPoints[i * 2 + 1] = renderDestinationPoints[i * 2 + 1] * frameHeight
        }

        fun distance(x0: Float, y0: Float, x1: Float, y1: Float): Int {
            return hypot(x1 - x0.toDouble(), y1 - y0.toDouble()).roundToInt()
        }
        // Get the length of the sides of the transformed rectangle in terms of pixels.
        renderWidth = distance(
            renderDestinationPoints[0],
            renderDestinationPoints[1],
            renderDestinationPoints[2],
            renderDestinationPoints[3]
        )
        renderHeight = distance(
            renderDestinationPoints[0],
            renderDestinationPoints[1],
            renderDestinationPoints[4],
            renderDestinationPoints[5]
        )
    }

    private fun prepareShader(texMatrix: FloatArray) {
        if (program == -1) {
            // Allocate new shader.
            program = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_OES)
            GLES20.glUseProgram(program)
            GlUtil.checkGlError("Failed to create program")

            GLES20.glUniform1i(GLES20.glGetUniformLocation(program, INPUT_TEXTURE_NAME), 0)
            GlUtil.checkGlError("Failed to setup program texture inputs")

            // Get input locations
            textureMatrixLocation = GLES20.glGetUniformLocation(program, TEXTURE_MATRIX_NAME)
            vertexPositionLocation =
                GLES20.glGetAttribLocation(program, INPUT_VERTEX_COORDINATE_NAME)
            textureCoordinateLocation =
                GLES20.glGetAttribLocation(program, INPUT_TEXTURE_COORDINATE_NAME)
            if (textureMatrixLocation == -1 || vertexPositionLocation == -1 || textureCoordinateLocation == -1) {
                throw InvalidParameterException("Failed to get shader locations")
            }
        }
        GLES20.glUseProgram(program)
        GlUtil.checkGlError("Failed to use program")

        // Upload the vertex coordinates.
        GLES20.glEnableVertexAttribArray(vertexPositionLocation)
        GLES20.glVertexAttribPointer(
            vertexPositionLocation, 2, GLES20.GL_FLOAT,
            false, 0, GlUtil.FULL_RECTANGLE_VERTEX_COORDINATES
        )

        // Upload the texture coordinates.
        GLES20.glEnableVertexAttribArray(textureCoordinateLocation)
        GLES20.glVertexAttribPointer(
            textureCoordinateLocation, 2, GLES20.GL_FLOAT,
            false, 0, GlUtil.FULL_RECTANGLE_TEXTURE_COORDINATES
        )

        // Upload the texture transformation matrix.
        GLES20.glUniformMatrix4fv(textureMatrixLocation, 1, false, texMatrix, 0)
        GlUtil.checkGlError("Failed to upload shader inputs")
    }

    override fun release() {
        if (program != -1) {
            GLES20.glUseProgram(0)
            GLES20.glDeleteProgram(program)
            program = -1
        }
    }

    companion object {
        // These points are used to calculate the size of the part of the frame we are rendering.
        private val srcPoints = floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f)

        private val INPUT_VERTEX_COORDINATE_NAME = "aPosition"
        private val INPUT_TEXTURE_COORDINATE_NAME = "aTextureCoordinate"
        private val TEXTURE_MATRIX_NAME = "uTextureMatrix"
        private val VERTEX_SHADER =
            """
        varying vec2 vTextureCoordinate;
        attribute vec4 aPosition;
        attribute vec4 aTextureCoordinate;
        uniform mat4 uTextureMatrix;
        void main() {
            gl_Position = aPosition;
            vTextureCoordinate = (uTextureMatrix * aTextureCoordinate).xy;
        }
        """

        private val INPUT_TEXTURE_NAME = "sTexture"
        private val FRAGMENT_SHADER_OES =
            """
        #extension GL_OES_EGL_image_external : require
        precision mediump float;
        varying vec2 vTextureCoordinate;
        uniform samplerExternalOES sTexture;
        void main() {
            vec4 Color = texture2D(sTexture, vTextureCoordinate);
            gl_FragColor = vec4(vec3(Color.r + Color.g + Color.b) / 3.0, Color.a);
        }
        """
    }
}


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

package com.amazonaws.services.chime.sdkdemo.utils

import android.graphics.Matrix
import android.opengl.EGL14
import android.opengl.GLES20
import android.os.Handler
import android.os.HandlerThread
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameRGBABuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.DefaultGlVideoFrameDrawer
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.GlTextureFrameBufferHelper
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.JniUtil

/**
 * [CpuVideoProcessor] draws frames to RGBA, converts to CPU, and then applies a simple black and white filter before forwarding
 */
class CpuVideoProcessor(private val logger: Logger, eglCoreFactory: EglCoreFactory) : VideoSource,
    VideoSink {
    // The camera capture source currently output OES texture frames, so we draw them to a frame buffer that
    // we can read to host memory from
    private val rectDrawer = DefaultGlVideoFrameDrawer()
    // Helper which wraps an OpenGLES texture frame buffer with resize capabilities
    private val textureFrameBuffer = GlTextureFrameBufferHelper(GLES20.GL_RGBA)

    override val contentHint: VideoContentHint = VideoContentHint.Motion

    // State necessary for EGL operations
    private lateinit var eglCore: EglCore
    private val thread: HandlerThread = HandlerThread("DemoCpuVideoProcessor")
    private val handler: Handler

    // Downstream video sinks
    private val sinks = mutableSetOf<VideoSink>()

    private val DUMMY_PBUFFER_OFFSET = 0

    private val TAG = "DemoCpuVideoProcessor"

    init {
        thread.start()
        handler = Handler(thread.looper)

        handler.post {
            eglCore = eglCoreFactory.createEglCore()

            // We need to create a dummy surface before we can set the context as current
            val surfaceAttribs = intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE)
            eglCore.eglSurface = EGL14.eglCreatePbufferSurface(
                eglCore.eglDisplay,
                eglCore.eglConfig,
                surfaceAttribs,
                DUMMY_PBUFFER_OFFSET
            )
            EGL14.eglMakeCurrent(
                eglCore.eglDisplay,
                eglCore.eglSurface,
                eglCore.eglSurface,
                eglCore.eglContext
            )
            GlUtil.checkGlError("Failed to set dummy surface to initialize surface texture video source")

            logger.info(TAG, "Created demo CPU video processor")
        }
    }

    override fun addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    override fun removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        frame.retain()
        handler.post {
            // Note: This processor assumes that the incoming call will be on a valid EGL context
            textureFrameBuffer.setSize(frame.getRotatedWidth(), frame.getRotatedHeight())
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.frameBufferId)

            val matrix = Matrix()
            // Shift before flipping
            matrix.preTranslate(0.5f, 0.5f)
            // RGBA frames are upside down relative to texture coordinates
            matrix.preScale(1f, -1f)
            // Unshift following flip
            matrix.preTranslate(-0.5f, -0.5f)
            // Note the draw call will account for any rotation, so we need to account for that in viewport width/height
            rectDrawer.drawFrame(
                frame,
                0,
                0,
                frame.getRotatedWidth(),
                frame.getRotatedHeight(),
                matrix
            )

            // Read RGBA data to native byte buffer
            val rgbaData = JniUtil.nativeAllocateByteBuffer(frame.width * frame.height * 4)
            GLES20.glReadPixels(
                0,
                0,
                frame.getRotatedWidth(),
                frame.getRotatedHeight(),
                GLES20.GL_RGBA,
                GLES20.GL_UNSIGNED_BYTE,
                rgbaData
            )
            GlUtil.checkGlError("glReadPixels")

            val rgbaBuffer =
                VideoFrameRGBABuffer(
                    frame.getRotatedWidth(),
                    frame.getRotatedHeight(),
                    rgbaData, frame.getRotatedWidth() * 4,
                    Runnable { JniUtil.nativeFreeByteBuffer(rgbaData) })

            convertToBlackAndWhite(rgbaBuffer)

            val processedFrame = VideoFrame(frame.timestampNs, rgbaBuffer)
            frame.release()

            sinks.forEach { it.onVideoFrameReceived(processedFrame) }
            processedFrame.release()
        }
    }

    fun release() {
        handler.post {
            logger.info(TAG, "Releasing CPU video processor source")

            rectDrawer.release()
            textureFrameBuffer.release()
            eglCore.release()

            handler.looper.quit()
        }
    }

    // This is of course an extremely inefficient way of converting to black and white
    private fun convertToBlackAndWhite(rgbaBuffer: VideoFrameRGBABuffer) {
        // So we don't need to pollute with @ExperimentalUnsignedTypes annotation
        fun Byte.toPositiveInt() = toInt() and 0xFF

        for (x in 0 until rgbaBuffer.width) {
            for (y in 0 until rgbaBuffer.height) {
                val rLocation = y * rgbaBuffer.stride + x * 4
                val gLocation = rLocation + 1
                val bLocation = rLocation + 2

                val rValue = rgbaBuffer.data[rLocation].toPositiveInt()
                val gValue = rgbaBuffer.data[gLocation].toPositiveInt()
                val bValue = rgbaBuffer.data[bLocation].toPositiveInt()

                val newValue = ((rValue + gValue + bValue) / (3.0)).toByte()

                rgbaBuffer.data.put(rLocation, newValue)
                rgbaBuffer.data.put(gLocation, newValue)
                rgbaBuffer.data.put(bLocation, newValue)
            }
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.utils

import android.content.Context
import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.utils.DefaultModality
import com.amazonaws.services.chime.sdk.meetings.utils.ModalityType
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.util.Date

fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
    return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}

fun AppCompatActivity.showToast(msg: String) {
    Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG).show()
}

fun encodeURLParam(string: String?): String {
    return URLEncoder.encode(string, "utf-8")
}

fun isLandscapeMode(context: Context?): Boolean {
    return context?.let { it.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE } ?: false
}

fun formatTimestamp(timestamp: Long): String {
    return SimpleDateFormat("hh:mm:ss a").format(Date(timestamp))
}

fun String.isContentShare(): Boolean {
    return DefaultModality(this).hasModality(ModalityType.Content)
}

fun RemoteVideoSource.isContentShare(): Boolean {
    return this.attendeeId.isContentShare()
}


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

package com.amazonaws.services.chime.sdkdemo.utils

import android.opengl.EGL14
import android.opengl.GLES20
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.DefaultGlVideoFrameDrawer
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil
import com.amazonaws.services.chime.sdk.meetings.utils.GlTextureFrameBufferHelper
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking

/**
 * [GpuVideoProcessor] is a simple demo processor which draws incoming frames onto a new surface with
 * a black and white filter. It also draws the original image into a corner of the screen.
 *
 * To pass along the image, it maintains its own internal OpenGLES texture (the texture which is drawn to)
 * which it will package as a [VideoFrame] and forward downstream, only updating the texture
 * (and creating the next [VideoFrame] to pass downstream) when the previous has been released.
 */
class GpuVideoProcessor(private val logger: Logger, eglCoreFactory: EglCoreFactory) : VideoSource,
    VideoSink {
    override val contentHint: VideoContentHint = VideoContentHint.Motion

    // Pending frame to render. Serves as a queue with size 1. Synchronized on `pendingFrameLock`.
    // pendingFrameLock also protects the handler (which may be null in `onVideoFrameReceived`
    private var pendingFrame: VideoFrame? = null
    private val pendingFrameLock = Any()

    // Drawers used by this processor
    private val bwDrawer = BlackAndWhiteGlVideoFrameDrawer()
    private val rectDrawer = DefaultGlVideoFrameDrawer()

    // Helper which wraps an OpenGLES texture frame buffer with resize capabilities
    private lateinit var textureFrameBuffer: GlTextureFrameBufferHelper

    // State necessary for EGL operations
    private lateinit var eglCore: EglCore
    private val thread: HandlerThread = HandlerThread("DemoGpuVideoProcessor")
    private var handler: Handler? = null

    // Texture is in use, possibly in another thread
    private var textureInUse = false

    // Dispose has been called and we are waiting on texture to be released
    private var released = false

    // Downstream video sinks
    private val sinks = mutableSetOf<VideoSink>()

    private val DUMMY_PBUFFER_OFFSET = 0

    private val TAG = "DemoGpuVideoProcessor"

    init {
        thread.start()
        handler = Handler(thread.looper)

        handler?.post {
            eglCore = eglCoreFactory.createEglCore()

            // We need to create a dummy surface before we can set the cotext as current
            val surfaceAttribs = intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE)
            eglCore.eglSurface = EGL14.eglCreatePbufferSurface(
                eglCore.eglDisplay,
                eglCore.eglConfig,
                surfaceAttribs,
                DUMMY_PBUFFER_OFFSET
            )
            EGL14.eglMakeCurrent(
                eglCore.eglDisplay,
                eglCore.eglSurface,
                eglCore.eglSurface,
                eglCore.eglContext
            )
            GlUtil.checkGlError("Failed to set dummy surface to initialize surface texture video source")

            textureFrameBuffer = GlTextureFrameBufferHelper(GLES20.GL_RGBA)

            logger.info(TAG, "Created demo GPU video processor")
        }
    }

    fun release() {
        handler?.post {
            logger.info(TAG, "Releasing GPU video processor source")
            released = true
            // We cannot release until no downstream users have access to texture buffer
            if (!textureInUse) {
                completeRelease()
            }
        }
    }

    override fun addVideoSink(sink: VideoSink) {
        handler?.post {
            sinks.add(sink)
        }
    }

    override fun removeVideoSink(sink: VideoSink) {
        val validHandler = handler ?: return // Already released
        runBlocking(validHandler.asCoroutineDispatcher().immediate) {
            sinks.remove(sink)
        }
    }

    override fun onVideoFrameReceived(frame: VideoFrame) {
        synchronized(pendingFrameLock) {
            if (pendingFrame != null) {
                // Release pending frame so we don't block any capture source upstream
                pendingFrame?.release()
            }

            if (handler != null) {
                pendingFrame = frame
                pendingFrame?.retain()
                handler?.post(::tryCapturingFrame)
            }
        }
    }

    private fun tryCapturingFrame() {
        check(Looper.myLooper() == handler?.looper)
        // Fetch and render |pendingFrame|.
        var frame: VideoFrame
        synchronized(pendingFrameLock) {
            if (pendingFrame == null) {
                return
            }
            frame = pendingFrame as VideoFrame
            pendingFrame = null
        }

        // Update state
        if (released || textureInUse) {
            frame.release()
            return
        }
        textureInUse = true

        textureFrameBuffer.setSize(frame.getRotatedWidth(), frame.getRotatedHeight())
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.frameBufferId)

        // Convert to black and white
        bwDrawer.drawFrame(frame, 0, 0, frame.getRotatedWidth(), frame.getRotatedHeight(), null)
        // Draw the original frame in the bottom left corner
        rectDrawer.drawFrame(
            frame,
            0,
            0,
            frame.getRotatedWidth() / 2,
            frame.getRotatedHeight() / 2,
            null
        )

        // Must call this otherwise downstream users will not have a synchronized texture
        GLES20.glFinish()
        // Reset to default framebuffer
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)

        val processedBuffer =
            VideoFrameTextureBuffer(
                frame.getRotatedWidth(),
                frame.getRotatedHeight(),
                textureFrameBuffer.textureId,
                null,
                VideoFrameTextureBuffer.Type.TEXTURE_2D,
                Runnable { frameReleased() })
        // Drawer gets rid of any rotation
        val processedFrame = VideoFrame(frame.timestampNs, processedBuffer)

        sinks.forEach { it.onVideoFrameReceived(processedFrame) }
        processedFrame.release()
        frame.release()
    }

    // Called once texture buffer ref count reaches 0
    private fun frameReleased() {
        // Cannot assume this occurs on correct thread
        handler?.post {
            textureInUse = false
            if (released) {
                this.completeRelease()
            } else {
                // May have pending frame
                tryCapturingFrame()
            }
        }
    }

    private fun completeRelease() {
        check(Looper.myLooper() == handler?.looper)

        rectDrawer.release()
        bwDrawer.release()
        textureFrameBuffer.release()
        eglCore.release()

        synchronized(pendingFrameLock) {
            if (pendingFrame != null) {
                pendingFrame?.release()
                pendingFrame = null
            }

            handler?.looper?.quit()
            handler = null
        }
    }
}


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

package com.amazonaws.services.chime.sdkdemo.utils

import com.amazonaws.services.chime.sdk.meetings.utils.logger.LogLevel

/**
 * Log format for the server
 *
 * @property sequenceNumber sequence number of the log
 * @property message message to log
 * @property timestampMs time of when log occurred
 * @property logLevel level of log
 */
data class LogEntry(
    val sequenceNumber: Int,
    val message: String,
    val timestampMs: Long,
    val logLevel: LogLevel
)


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

package com.amazonaws.services.chime.sdkdemo.utils

import android.util.Log
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.utils.logger.LogLevel
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.google.gson.Gson
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.Calendar
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class PostLogger(
    private val name: String,
    private val configuration: MeetingSessionConfiguration,
    private val url: String,
    private var level: LogLevel = LogLevel.INFO
) : Logger {
    private val gson = Gson()
    private val uiScope = CoroutineScope(Dispatchers.Main)
    private var sequenceNumber = AtomicInteger(0)
    private val logEntries: MutableList<LogEntry> = mutableListOf()

    override fun verbose(tag: String, msg: String) {
        log(tag, msg, LogLevel.VERBOSE)
    }

    override fun debug(tag: String, msg: String) {
        log(tag, msg, LogLevel.DEBUG)
    }

    override fun info(tag: String, msg: String) {
        log(tag, msg, LogLevel.INFO)
    }

    override fun warn(tag: String, msg: String) {
        log(tag, msg, LogLevel.WARN)
    }

    override fun error(tag: String, msg: String) {
        log(tag, msg, LogLevel.ERROR)
    }

    override fun setLogLevel(level: LogLevel) {
        this.level = level
    }

    override fun getLogLevel(): LogLevel {
        return level
    }

    private fun saveLog(msg: String, type: LogLevel) {
        logEntries.add(LogEntry(sequenceNumber.get(), msg, Calendar.getInstance().timeInMillis, type))
        sequenceNumber.incrementAndGet()
    }

    fun publishLog(tag: String) {
        val body = makeRequestBody(logEntries)
        uiScope.launch {
            makeRequest(body, tag)
        }
    }

    private fun log(tag: String, msg: String, type: LogLevel) {
        if (type.priority < this.level.priority) return
        saveLog(msg, type)
    }

    private suspend fun makeRequest(body: String, tag: String) {
        withContext(Dispatchers.IO) {
            try {
                val serverUrl = URL(url)
                val response = StringBuffer()
                with(serverUrl.openConnection() as HttpURLConnection) {
                    requestMethod = "POST"
                    doInput = true
                    doOutput = true
                    setRequestProperty("Accept", "application/json")

                    outputStream.use {
                        val input = body.toByteArray()
                        it.write(input, 0, input.size)
                    }

                    BufferedReader(InputStreamReader(inputStream)).use {
                        var inputLine = it.readLine()
                        while (inputLine != null) {
                            response.append(inputLine)
                            inputLine = it.readLine()
                        }
                        it.close()
                    }

                    if (responseCode == 200) {
                        Log.i(tag, "Publishing log was successful")
                        response.toString()
                        logEntries.clear()
                    } else {
                        Log.e(tag, "Unable to publish log. Response code: $responseCode")
                        null
                    }
                }
            } catch (exception: Exception) {
                Log.e(tag, "There was an exception while posting logs: $exception")
                null
            }
        }
    }

    private fun makeRequestBody(batch: MutableList<LogEntry>): String {
        val body = mutableMapOf(
            "meetingId" to configuration.meetingId,
            "attendeeId" to configuration.credentials.attendeeId,
            "appName" to name,
            "logs" to batch
        )
        return gson.toJson(body)
    }
}


package com.amazonaws.ivs.reactnative.player

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class AmazonIvsPackage : ReactPackage {
    override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
        return emptyList()
    }

    override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
      return listOf<ViewManager<*, *>>(AmazonIvsViewManager())
    }
}


package com.amazonaws.ivs.reactnative.player

import android.content.pm.PackageManager
import android.net.Uri
import android.widget.FrameLayout
import com.amazonaws.ivs.player.*
import android.os.Build
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.events.RCTEventEmitter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.timerTask
import android.app.PictureInPictureParams
import android.app.Activity
import androidx.annotation.RequiresApi

class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(context), LifecycleEventListener {
  private var playerView: PlayerView? = null
  private var player: Player? = null
  private var streamUri: Uri? = null
  private val playerListener: Player.Listener?

  var playerObserver: Timer? = null
  private var lastLiveLatency: Long? = null
  private var lastBitrate: Long? = null
  private var lastDuration: Long? = null
  private var finishedLoading: Boolean = false

  enum class Events(private val mName: String) {
    STATE_CHANGED("onPlayerStateChange"),
    DURATION_CHANGED("onDurationChange"),
    ERROR("onError"),
    QUALITY_CHANGED("onQualityChange"),
    CUE("onTextCue"),
    METADATA_CUE("onTextMetadataCue"),
    LOAD("onLoad"),
    LOAD_START("onLoadStart"),
    REBUFFERING("onRebuffering"),
    SEEK("onSeek"),
    DATA("onData"),
    LIVE_LATENCY_CHANGED("onLiveLatencyChange"),
    VIDEO_STATISTICS("onVideoStatistics"),
    PROGRESS("onProgress");

    override fun toString(): String {
      return mName
    }
  }

  init {
    playerView = PlayerView(context)
    player = playerView?.player
    playerView?.controlsEnabled = false

    (context as ThemedReactContext).addLifecycleEventListener(this)

    playerListener = object : Player.Listener() {
      override fun onStateChanged(state: Player.State) {
        onPlayerStateChange(state)
      }

      override fun onDurationChanged(duration: Long) {
        onDurationChange(duration)
      }

      override fun onRebuffering() {
        onPlayerRebuffering()
      }

      override fun onSeekCompleted(position: Long) {
        onSeek(position)
      }

      override fun onQualityChanged(quality: Quality) {
        onQualityChange(quality)
      }

      override fun onVideoSizeChanged(p0: Int, p1: Int) {
        post(mLayoutRunnable)
      }

      override fun onCue(cue: Cue) {
        if (cue is TextCue) {
          onTextCue(cue)
        } else if (cue is TextMetadataCue) {
          onTextMetadataCue(cue)
        }
      }

      override fun onError(e: PlayerException) {
        onError(e.getErrorMessage())
      }
    }

    player?.addListener(playerListener);
    addView(playerView)

    playerObserver = Timer("observerInterval", false)
    playerObserver?.schedule(timerTask {
      intervalHandler()
    }, 0, 1000)
  }

  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    if (changed) {
      post(mLayoutRunnable)
    }
  }

  fun setStreamUrl(streamUrl: String) {
    player?.let { player ->
      val reactContext = context as ReactContext
      val uri = Uri.parse(streamUrl);
      this.streamUri = uri;

      finishedLoading = false
      player.load(uri)

      reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.LOAD_START.toString(), Arguments.createMap())
    }
  }

  fun setMuted(muted: Boolean) {
    player?.isMuted = muted
  }

  fun setVolume(volume: Double) {
    player?.setVolume(volume.toFloat())
  }

  fun setLiveLowLatency(liveLowLatency: Boolean) {
    player?.setLiveLowLatencyEnabled(liveLowLatency)
  }

  fun setPlaybackRate(playbackRate: Double) {
    player?.playbackRate = playbackRate.toFloat()
  }

  fun setLogLevel(logLevel: Double) {
    when (logLevel.toInt()) {
      0 -> player?.setLogLevel(Player.LogLevel.DEBUG)
      1 -> player?.setLogLevel(Player.LogLevel.INFO)
      2 -> player?.setLogLevel(Player.LogLevel.WARNING)
      3 -> player?.setLogLevel(Player.LogLevel.ERROR)
    }
  }

  fun setResizeMode(resizeMode: String?) {
    playerView?.resizeMode = findResizeMode(resizeMode)
  }

  private fun findResizeMode(mode: String?): ResizeMode = when (mode) {
      "aspectFill" -> ResizeMode.FILL
      "aspectFit" -> ResizeMode.FIT
      "aspectZoom" -> ResizeMode.ZOOM
      else -> ResizeMode.FIT
  }

  private fun findQuality(quality: ReadableMap?): Quality? {
    val newQuality = player?.qualities?.firstOrNull() { x ->
        x.name == quality?.getString("name") &&
        x.codecs == quality.getString("codecs") &&
        x.bitrate == quality.getInt("bitrate") &&
        x.framerate == quality.getDouble("framerate").toFloat() &&
        x.width == quality.getInt("width") &&
        x.height == quality.getInt("height")
    }

    return newQuality
  }

  fun setQuality(quality: ReadableMap?) {
    if (quality != null) {
      findQuality(quality)?.let {
        player?.quality = it
      }
    }
  }

  fun setAutoMaxQuality(quality: ReadableMap?) {
    if (quality != null) {
      findQuality(quality)?.let {
        player?.setAutoMaxQuality(it)
      }
    }
  }

  fun setAutoQualityMode(autoQualityMode: Boolean) {
    player?.isAutoQualityMode = autoQualityMode
  }

  fun onTextCue(cue: TextCue) {
    val reactContext = context as ReactContext

    val textCue = Arguments.createMap()
    textCue.putString("type", cue.javaClass.name)
    textCue.putDouble("line", cue.line.toDouble())
    textCue.putDouble("size", cue.size.toDouble())
    textCue.putDouble("position", cue.position.toDouble())
    textCue.putString("text", cue.text)
    textCue.putInt("textAlignment", cue.textAlign.ordinal)

    val data = Arguments.createMap()
    data.putMap("textCue", textCue)

    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.CUE.toString(), data)
  }

  fun setMaxBitrate(bitrate: Double) {
    player?.setAutoMaxBitrate(bitrate.toInt())
  }

  fun setInitialBufferDuration(duration: Double) {
    val valueInMilliseconds = duration * 1000
    player?.setInitialBufferDuration(valueInMilliseconds.toLong())
  }

  fun onTextMetadataCue(cue: TextMetadataCue) {
    val reactContext = context as ReactContext

    val textMetadataCue = Arguments.createMap()
    textMetadataCue.putString("type", cue.javaClass.name)
    textMetadataCue.putString("text", cue.text)
    textMetadataCue.putString("description", cue.description)

    val data = Arguments.createMap()
    data.putMap("textMetadataCue", textMetadataCue)

    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.METADATA_CUE.toString(), data)
  }

  fun onDurationChange(duration: Long) {
    val reactContext = context as ReactContext
    val data = Arguments.createMap()
    val parsedDuration = getDuration(duration);
    data.putDouble("duration", parsedDuration)

    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.DURATION_CHANGED.toString(), data)
  }

  fun onError(error: String) {
    val reactContext = context as ReactContext
    val data = Arguments.createMap()
    data.putString("error", error)

    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.ERROR.toString(), data)
  }

  fun onSeek(position: Long) {
    val reactContext = context as ReactContext
    val data = Arguments.createMap()
    data.putDouble("position", convertMilliSecondsToSeconds(position))
    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.SEEK.toString(), data)
  }

  fun onProgress(position: Long) {
    val reactContext = context as ReactContext
    val data = Arguments.createMap()
    data.putDouble("position", convertMilliSecondsToSeconds(position))
    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.PROGRESS.toString(), data)
  }

  private fun convertMilliSecondsToSeconds (milliSeconds: Long): Double {
    return milliSeconds / 1000.0
  }

  private val mLayoutRunnable = Runnable {
    measure(
      MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
      MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
    layout(left, top, right, bottom)
  }

  fun play() {
    player?.play()
  }

  fun pause() {
    player?.pause()
  }

  fun seekTo(position: Double) {
    val milliseconds = (position * 1000.0).toLong()
    player?.seekTo(milliseconds)
  }

  fun setOrigin(origin: String) {
    player?.setOrigin(origin)
  }

  fun onPlayerStateChange(state: Player.State) {
    val reactContext = context as ReactContext

    when (state) {
      Player.State.PLAYING -> {
        if (!finishedLoading) {
          val onLoadData = Arguments.createMap()
          val parsedDuration = getDuration(player!!.duration);
          onLoadData.putDouble("duration", parsedDuration)

          finishedLoading = true

          reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.LOAD.toString(), onLoadData)
        }
      }
      Player.State.READY -> {
        val data = Arguments.createMap()
        val playerData = Arguments.createMap()
        playerData.putString("version", player?.version)
        playerData.putString("sessionId", player?.sessionId)

        val qualities = Arguments.createArray()
        for (quality in player!!.qualities) {
          val parsedQuality = Arguments.createMap()
          parsedQuality.putString("name", quality.name)
          parsedQuality.putString("codecs", quality.codecs)
          parsedQuality.putInt("bitrate", quality.bitrate)
          parsedQuality.putDouble("framerate", quality.framerate.toDouble())
          parsedQuality.putInt("width", quality.width)
          parsedQuality.putInt("height", quality.height)
          qualities.pushMap(parsedQuality)
        }
        playerData.putArray("qualities", qualities)
        data.putMap("playerData", playerData)

        reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.DATA.toString(), data)
      }
      else -> {}
    }

    val onStateChangeData = Arguments.createMap()
    onStateChangeData.putString("state", mapPlayerState(state))
    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.STATE_CHANGED.toString(), onStateChangeData)
  }

  fun onQualityChange(quality: Quality) {
    val reactContext = context as ReactContext

    val newQuality = Arguments.createMap()
    newQuality.putString("name", quality.name)
    newQuality.putString("codecs", quality.codecs)
    newQuality.putInt("bitrate", quality.bitrate)
    newQuality.putDouble("framerate", quality.framerate.toDouble())
    newQuality.putInt("width", quality.width)
    newQuality.putInt("height", quality.height)

    val data = Arguments.createMap()
    data.putMap("quality", newQuality)

    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.QUALITY_CHANGED.toString(), data)
  }

  fun onPlayerRebuffering() {
    val reactContext = context as ReactContext
    reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.REBUFFERING.toString(), Arguments.createMap())
  }

  private fun intervalHandler() {
    val reactContext = context as ReactContext

    if (lastLiveLatency != player?.liveLatency) {
      val liveLatencyData = Arguments.createMap()

      player?.liveLatency?.let { liveLatency ->
        liveLatencyData.putInt("liveLatency", liveLatency.toInt())
      } ?: run {
        liveLatencyData.putNull("liveLatency")
      }
      reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.LIVE_LATENCY_CHANGED.toString(), liveLatencyData)

      lastLiveLatency = player?.liveLatency
    }

    if (lastBitrate != player?.averageBitrate || lastDuration != player?.duration) {
      val onVideoData = Arguments.createMap()
      val videoData = Arguments.createMap()

      player?.duration?.let { duration ->
        val parsedDuration = getDuration(duration)
        videoData.putDouble("duration", parsedDuration)
      } ?: run {
        videoData.putNull("duration")
      }

      player?.averageBitrate?.let { averageBitrate ->
        videoData.putInt("bitrate", averageBitrate.toInt())
      } ?: run {
        videoData.putNull("bitrate")
      }

      onVideoData.putMap("videoData", videoData)
      reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.VIDEO_STATISTICS.toString(), onVideoData)

      lastBitrate = player?.averageBitrate
      lastDuration = player?.duration
    }
    player?.position?.let { position ->
      if (position > 0 && player?.state === Player.State.PLAYING) {
        onProgress(position)
      }
    }
  }

  private fun getDuration(duration: Long): Double {
    return convertMilliSecondsToSeconds(duration)
  }

  private fun mapPlayerState(state: Player.State): String {
    return when(state) {
      Player.State.PLAYING -> "Playing"
      Player.State.BUFFERING -> "Buffering"
      Player.State.READY -> "Ready"
      Player.State.IDLE -> "Idle"
      Player.State.ENDED -> "Ended"
    }
  }

  fun togglePip(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
      && context.packageManager
        .hasSystemFeature(
          PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
      val activity: Activity? = context.currentActivity
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val params = PictureInPictureParams.Builder()
        activity?.enterPictureInPictureMode(params.build());
      } else {
        activity?.enterPictureInPictureMode();
      }

    }
  }

  override fun onHostResume() {
  }

  override fun onHostPause() {}

  override fun onHostDestroy() {
    cleanup()
  }

  fun cleanup() {
    player?.removeListener(playerListener!!)
    player?.release()
    player = null

    playerObserver?.cancel()
    playerObserver = null
  }
}


package com.amazonaws.ivs.reactnative.player

import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp

class AmazonIvsViewManager : SimpleViewManager<AmazonIvsView>() {
  private enum class Commands {
    PLAY,
    PAUSE,
    SEEK_TO,
    SET_ORIGIN,
    TOGGLE_PIP
  }

  override fun getName() = "AmazonIvs"

  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Map<String, String>>? {
    val builder: MapBuilder.Builder<String, Map<String, String>> = MapBuilder.builder<String, Map<String, String>>()
    for (event in AmazonIvsView.Events.values()) {
      builder.put(event.toString(), MapBuilder.of("registrationName", event.toString()))
    }
    return builder.build()
  }

  override fun getCommandsMap(): Map<String, Int>? {
    return MapBuilder.of(
      "play",
      Commands.PLAY.ordinal,
      "pause",
      Commands.PAUSE.ordinal,
      "seekTo",
      Commands.SET_ORIGIN.ordinal,
      "setOrigin",
      Commands.SEEK_TO.ordinal,
      "togglePip",
      Commands.TOGGLE_PIP.ordinal
    )
  }

  override fun receiveCommand(view: AmazonIvsView, commandType: Int, args: ReadableArray?) {
    when (commandType) {
      Commands.PLAY.ordinal -> view.play()
      Commands.PAUSE.ordinal -> view.pause()
      Commands.TOGGLE_PIP.ordinal -> view.togglePip()
      Commands.SET_ORIGIN.ordinal -> {
        args?.getString(0)?.let {
          origin ->
            view.setOrigin(origin)
        }
      }
      Commands.SEEK_TO.ordinal -> {
        args?.getDouble(0)?.let { position ->
          view.seekTo(position)
        }
      }
      else -> {
      }
    }
  }

  @ReactProp(name = "streamUrl")
  fun setStreamUrl(view: AmazonIvsView, streamUrl: String) {
    view.setStreamUrl(streamUrl);
  }

  @ReactProp(name = "resizeMode")
  fun setResizeMode(view: AmazonIvsView, mode: String) {
    view.setResizeMode(mode);
  }

  @ReactProp(name = "muted")
  fun setMuted(view: AmazonIvsView, muted: Boolean) {
    view.setMuted(muted)
  }

  @ReactProp(name = "volume")
  fun setVolume(view: AmazonIvsView, volume: Double) {
    view.setVolume(volume)
  }

  @ReactProp(name = "liveLowLatency")
  fun setLiveLowLatency(view: AmazonIvsView, liveLowLatency: Boolean) {
    view.setLiveLowLatency(liveLowLatency)
  }

  @ReactProp(name = "playbackRate")
  fun setPlaybackRate(view: AmazonIvsView, playbackRate: Double) {
    view.setPlaybackRate(playbackRate)
  }

  @ReactProp(name = "logLevel")
  fun setLogLevel(view: AmazonIvsView, logLevel: Double) {
    view.setLogLevel(logLevel)
  }

  @ReactProp(name = "quality")
  fun setQuality(view: AmazonIvsView, quality: ReadableMap?) {
    view.setQuality(quality)
  }

  @ReactProp(name = "autoMaxQuality")
  fun setAutoMaxQuality(view: AmazonIvsView, quality: ReadableMap?) {
    view.setAutoMaxQuality(quality)
  }

  @ReactProp(name = "autoQualityMode")
  fun setAutoQualityMode(view: AmazonIvsView, autoQualityMode: Boolean) {
    view.setAutoQualityMode(autoQualityMode)
  }

  @ReactProp(name = "maxBitrate")
  fun setMaxBitrate(view: AmazonIvsView, bitrate: Double) {
    view.setMaxBitrate(bitrate)
  }

  @ReactProp(name = "initialBufferDuration")
  fun setInitialBufferDuration(view: AmazonIvsView, duration: Double) {
    view.setInitialBufferDuration(duration)
  }

  override fun createViewInstance(reactContext: ThemedReactContext): AmazonIvsView {
    return AmazonIvsView(reactContext)
  }

  override fun onDropViewInstance(view: AmazonIvsView) {
    super.onDropViewInstance(view)
    view.cleanup()
  }
}