diff --git a/AgoraCloudScene/build.gradle b/AgoraCloudScene/build.gradle index 322b83fc2..334a2dca2 100644 --- a/AgoraCloudScene/build.gradle +++ b/AgoraCloudScene/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.library' id 'kotlin-android' id 'kotlin-parcelize' + id 'com.google.protobuf' } android { @@ -49,6 +50,22 @@ android { } } + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.21.12' + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { + option 'lite' // 生成精简版类 + } + } + } + } +} + dependencies { //implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.core:core-ktx:${rootProject.ext.dependencies.core_ktx}" @@ -77,6 +94,8 @@ dependencies { implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:custom-ui:12.0.0' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" + + implementation 'com.google.protobuf:protobuf-javalite:3.21.12' } if (readyPublishMaven.toBoolean()) { diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/FcrRttToolBoxComponent.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/FcrRttToolBoxComponent.kt new file mode 100644 index 000000000..9de631c25 --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/FcrRttToolBoxComponent.kt @@ -0,0 +1,84 @@ +package io.agora.online.component + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import io.agora.agoraeducore.core.internal.framework.impl.managers.AgoraWidgetActiveObserver +import io.agora.agoraeducore.core.internal.framework.impl.managers.AgoraWidgetRoomPropsUpdateReq +import io.agora.online.component.common.AbsAgoraEduComponent +import io.agora.online.component.common.IAgoraUIProvider +import io.agora.online.databinding.FcrOnlineEduRttComponentBinding +import io.agora.online.options.AgoraEduOptionsComponent +import io.agora.online.options.AgoraEduRttOptionsComponent +import io.agora.online.widget.FcrWidgetManager.WIDGETS_RTT_ID +import io.agora.online.widget.rtt.FcrRttToolBoxWidget + +class FcrRttToolBoxComponent : AbsAgoraEduComponent { + constructor(context: Context) : super(context) + constructor(context: Context, attr: AttributeSet) : super(context, attr) + constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) + + + private var binding: FcrOnlineEduRttComponentBinding = FcrOnlineEduRttComponentBinding.inflate(LayoutInflater.from(context), this, true) + + /** + * widget插件 + */ + private var widget: FcrRttToolBoxWidget? = null + + /** + * 界面上设置好的布局 + */ + private var conversionStatusView: ViewGroup? = null + private var subtitleView: AgoraEduRttOptionsComponent? = null + private var agoraEduOptionsComponent: AgoraEduOptionsComponent? = null + + /** + * 插件注册监听 + */ + private val widgetActiveObserver = object : AgoraWidgetActiveObserver { + override fun onWidgetActive(widgetId: String) { + if (widget == null) { + val widgetConfig = eduContext?.widgetContext()?.getWidgetConfig(widgetId) + widgetConfig?.let { config -> + widget = eduContext?.widgetContext()?.create(config) as FcrRttToolBoxWidget? + runOnUIThread{ + widget?.init(binding.root, agoraUIProvider, agoraEduOptionsComponent!!, conversionStatusView!!, subtitleView!!) + } + } + } + } + + override fun onWidgetInActive(widgetId: String) { + ContextCompat.getMainExecutor(binding.root.context).execute { widget?.release() } + } + } + + /** + * 重置工具准提 + */ + fun resetEduRttToolBoxStatus() { + widget?.resetEduRttToolBoxStatus() + } + + fun initView( + agoraUIProvider: IAgoraUIProvider, agoraEduOptionsComponent: AgoraEduOptionsComponent, conversionStatusView: ViewGroup, + subtitleView: AgoraEduRttOptionsComponent, + ) { + super.initView(agoraUIProvider) + this.agoraEduOptionsComponent = agoraEduOptionsComponent + this.conversionStatusView = conversionStatusView + this.subtitleView = subtitleView + eduContext?.widgetContext()?.addWidgetActiveObserver(widgetActiveObserver, WIDGETS_RTT_ID) + eduContext?.widgetContext()?.setWidgetActive(WIDGETS_RTT_ID, AgoraWidgetRoomPropsUpdateReq(state = 1)) + } + + /** + * 如果体验结束的话,那么则隐藏弹窗view + */ + fun resetShowDialogIfEnd() { + widget?.resetShowDialogIfEnd() + } +} diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialog.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialog.kt index f8e800410..c3068780f 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialog.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialog.kt @@ -4,6 +4,7 @@ import android.app.Dialog import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.text.SpannableString import android.view.View import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView @@ -60,6 +61,13 @@ class AgoraUIDialog(context: Context) : Dialog(context, R.style.agora_full_scree this.message.visibility = View.VISIBLE this.message.text = message } + fun setMessage(message: SpannableString) { + this.message.visibility = View.VISIBLE + this.message.text = message + } + fun setMessagePaddingHorizontal(messagePaddingHorizontal: Int) { + this.message.setPadding(messagePaddingHorizontal,this.message.paddingTop,messagePaddingHorizontal,this.message.paddingBottom) + } fun setIconResource(iconResource: Int) { this.icon.visibility = View.VISIBLE diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialogBuilder.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialogBuilder.kt index 26b7243b0..08b32a83e 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialogBuilder.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIDialogBuilder.kt @@ -1,14 +1,17 @@ package io.agora.online.component.dialog import android.content.Context +import android.text.SpannableString import android.view.View class AgoraUIDialogBuilder(private val context: Context) { private var title: String? = null private var message: String? = null + private var messageSpan: SpannableString? = null private var positiveText: String? = null private var negativeText: String? = null private var iconResource: Int? = null + private var messagePaddingHorizontal: Int? = null private var positiveListener: View.OnClickListener? = null private var negativeListener: View.OnClickListener? = null private var mCancelable: Boolean? = null @@ -28,6 +31,16 @@ class AgoraUIDialogBuilder(private val context: Context) { return this } + fun message(message: SpannableString): AgoraUIDialogBuilder { + this.messageSpan = message + return this + } + + fun messagePaddingHorizontal(messagePaddingHorizontal: Int): AgoraUIDialogBuilder { + this.messagePaddingHorizontal = messagePaddingHorizontal + return this + } + fun positiveText(text: String): AgoraUIDialogBuilder { this.positiveText = text return this @@ -57,6 +70,8 @@ class AgoraUIDialogBuilder(private val context: Context) { val dialog = AgoraUIDialog(context) title?.let { dialog.setTitle(it) } message?.let { dialog.setMessage(it) } + messageSpan?.let { dialog.setMessage(it) } + messagePaddingHorizontal?.let { dialog.setMessagePaddingHorizontal(it) } positiveText?.let { dialog.setPositiveButtonText(it) } positiveListener?.let { dialog.setPositiveClick(it) } negativeText?.let { dialog.setNegativeButtonText(it) } @@ -138,4 +153,28 @@ class AgoraUICustomDialogBuilder(private val context: Context) { } return dialog } -} \ No newline at end of file +} + +class AgoraUIRttSettingBuilder(private val context: Context) { + private var listener: AgoraUIRttSettingDialogListener? = null + fun build(): AgoraUIRttSettingDialog { + val dialog = AgoraUIRttSettingDialog(context) + listener?.let { dialog.setListener(it) } + return dialog + } + + /** + * 设置监听 + */ + fun setListener(listener: AgoraUIRttSettingDialogListener): AgoraUIRttSettingBuilder { + this.listener = listener + return this + } +} + +class AgoraUIRttConversionDialogBuilder(private val context: Context) { + fun build(): AgoraUIRttConversionDialog { + val dialog = AgoraUIRttConversionDialog(context) + return dialog + } +} diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttConversionDialog.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttConversionDialog.kt new file mode 100644 index 000000000..5ec14114b --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttConversionDialog.kt @@ -0,0 +1,431 @@ +package io.agora.online.component.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.text.Spannable +import android.text.SpannableString +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import io.agora.online.R +import io.agora.online.databinding.FcrOnlineEduRttConversionDialogBinding +import io.agora.online.helper.RttLanguageEnum +import io.agora.online.helper.RttRecordItem +import java.text.MessageFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * 功能作用:Rtt实时转写显示弹窗 + * 创建人:王亮(Loren) + * 思路: + * 方法: + * 注意: + * 修改人: + * 修改时间: + * 备注: + * + * @author 王亮(Loren) + */ +class AgoraUIRttConversionDialog(context: Context) : Dialog(context, R.style.agora_full_screen_dialog) { + + private val binding: FcrOnlineEduRttConversionDialogBinding by lazy { + FcrOnlineEduRttConversionDialogBinding.inflate(layoutInflater, null, false) + } + + /** + * 操作回调 + */ + var optionsCallback: ConversionOptionsInterface? = null + + /** + * 适配器 + */ + private val mAdapter = RecordAdapter(context, arrayListOf()) + + init { + setContentView(binding.root) + val window = this.window + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + window?.decorView?.setBackgroundResource(android.R.color.transparent) + val layout = findViewById(R.id.agora_dialog_layout) + layout.elevation = 10f + + initView() + } + + @SuppressLint("SetTextI18n") + private fun initView() { + binding.fcrOnlineEduRttConversionDialogChangeStatus.setOnClickListener { + if (it.isActivated) { + closeConversion(true) + } else { + openConversion(true) + } + } + binding.fcrOnlineEduRttConversionDialogOptionsSetting.setOnClickListener { + optionsCallback?.openSetting() + } + binding.fcrOnlineEduRttConversionDialogOptionsClose.setOnClickListener { dismiss() } + binding.fcrOnlineEduRttConversionDialogList.adapter = mAdapter + binding.fcrOnlineEduRttConversionDialogList.layoutManager = LinearLayoutManager(context) + binding.fcrOnlineEduRttConversionDialogInput.setOnEditorActionListener { v, actionId, _ -> + if (EditorInfo.IME_ACTION_SEARCH == actionId) { + searchData(if (v.text != null) v.text.toString() else "") + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + binding.fcrOnlineEduRttConversionDialogOptionsGoPre.setOnClickListener { + if (mAdapter.currentSearchResultIndex > 0) { + mAdapter.currentSearchResultIndex-- + changeShowSearchResult() + } + } + binding.fcrOnlineEduRttConversionDialogOptionsGoNext.setOnClickListener { + if (mAdapter.currentSearchResultIndex + 1 < mAdapter.getSumResultCount()) { + mAdapter.currentSearchResultIndex++ + changeShowSearchResult() + } + } + binding.fcrOnlineEduRttConversionDialogSearchClear.setOnClickListener { + binding.fcrOnlineEduRttConversionDialogInput.text = null + binding.fcrOnlineEduRttConversionDialogInput.clearFocus() + searchData(null) + } + } + + /** + * 设置限时体验信息 + */ + fun setExperienceInfo(allowUseConfig: Boolean, rttExperienceReduceTime: Int) { + binding.fcrOnlineEduRttConversionDialogChangeStatus.isEnabled = allowUseConfig || rttExperienceReduceTime > 0 + if (allowUseConfig) { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.visibility = View.GONE + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.visibility = View.GONE + } else { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.visibility = View.VISIBLE + if (rttExperienceReduceTime <= 0) { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setText(R.string.fcr_dialog_rtt_time_limit_end) + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setBackgroundResource(R.drawable.agora_options_rtt_time_limit_bg_disable) + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.visibility = View.GONE + closeConversion(true) + } else { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setText(R.string.fcr_dialog_rtt_time_limit) + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setBackgroundResource(R.drawable.agora_options_rtt_time_limit_bg) + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.visibility = View.VISIBLE + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.text = MessageFormat.format( + context.resources.getString(R.string.fcr_dialog_rtt_dialog_time_limit_reduce), + if (rttExperienceReduceTime / 60000 > 9) rttExperienceReduceTime / 60000 else "0${rttExperienceReduceTime / 60000}", + if (rttExperienceReduceTime % 60000 / 1000 > 9) rttExperienceReduceTime % 60000 / 1000 else "0${rttExperienceReduceTime % 60000 / 1000}", + ) + } + } + } + + /** + * 刷新记录信息 + */ + fun updateShowList(list: List) { + binding.root.post { + if (mAdapter.dataList.size != list.size) { + mAdapter.dataList.clear() + mAdapter.dataList.addAll(list) + mAdapter.notifyItemRangeChanged(0, list.size) + if (mAdapter.searchText.isNullOrEmpty()) { + binding.fcrOnlineEduRttConversionDialogList.scrollToPosition(mAdapter.dataList.size - 1) + } + } else { + if(list.isNotEmpty() && mAdapter.dataList.isNotEmpty()) { + mAdapter.dataList[mAdapter.dataList.size - 1] = list[list.size - 1] + mAdapter.notifyItemChanged(mAdapter.dataList.size - 1) + if (mAdapter.searchText.isNullOrEmpty()) { + binding.fcrOnlineEduRttConversionDialogList.scrollToPosition(mAdapter.dataList.size - 1) + } + } + } + this.searchData(mAdapter.searchText) + } + } + + /** + * 修改按钮状态 + */ + fun changeShowButton(toState:Boolean){ + if(toState){ + openConversion(false) + }else{ + closeConversion(false) + } + } + + /** + * 开始转写 + */ + private fun openConversion(sendRequest:Boolean) { + if (binding.fcrOnlineEduRttConversionDialogChangeStatus.isEnabled) { + if (sendRequest) { + optionsCallback?.openConversion() + } + binding.fcrOnlineEduRttConversionDialogChangeStatus.isActivated = true + binding.fcrOnlineEduRttConversionDialogChangeStatus.setText(R.string.fcr_dialog_rtt_dialog_conversion_close) + binding.fcrOnlineEduRttConversionDialogChangeStatus.setTextColor(ContextCompat.getColor(context, R.color.fcr_text_red)) + } + } + + /** + * 关闭转写 + */ + private fun closeConversion(sendRequest:Boolean) { + if (sendRequest) { + optionsCallback?.closeConversion() + } + binding.fcrOnlineEduRttConversionDialogChangeStatus.isActivated = false + binding.fcrOnlineEduRttConversionDialogChangeStatus.setText(R.string.fcr_dialog_rtt_dialog_conversion_open) + binding.fcrOnlineEduRttConversionDialogChangeStatus.setTextColor(ContextCompat.getColor(context, R.color.fcr_white)) + } + + /** + * 搜索数据 + */ + @SuppressLint("SetTextI18n") + private fun searchData(text: String?) { + mAdapter.searchText = text + if (!text.isNullOrEmpty()) { + mAdapter.searchResultListIndexMap.clear() + mAdapter.dataList.forEachIndexed { index, rttRecordItem -> + val showText = rttRecordItem.getShowText() + val level1Text = showText[0] + val level2Text = showText[1] + findSubstringsIndexes(level1Text, text).apply { + addAll(findSubstringsIndexes(if (level2Text.isNullOrEmpty()) "" else level2Text, text).map { return@map it + (level1Text?.length ?: 0) }) + }.let { + if (it.isNotEmpty()) { + mAdapter.searchResultListIndexMap[index] = it + } + } + } + mAdapter.currentSearchResultIndex = + mAdapter.currentSearchResultIndex.coerceAtMost(mAdapter.itemCount - 1).coerceAtMost(mAdapter.getSumResultCount() - 1).coerceAtLeast(0) + binding.fcrOnlineEduRttConversionDialogSearchClear.visibility = View.VISIBLE + binding.fcrOnlineEduRttConversionDialogSearchCount.visibility = View.VISIBLE + binding.fcrOnlineEduRttConversionDialogOptionsChangeLoc.visibility = View.VISIBLE + changeShowSearchResult() + } else { + mAdapter.searchResultListIndexMap.clear() + mAdapter.currentSearchResultIndex = 0 + mAdapter.notifyItemRangeChanged(0, mAdapter.itemCount) + binding.fcrOnlineEduRttConversionDialogSearchClear.visibility = View.GONE + binding.fcrOnlineEduRttConversionDialogSearchCount.visibility = View.GONE + binding.fcrOnlineEduRttConversionDialogOptionsChangeLoc.visibility = View.GONE + } + } + + /** + * 显示弹窗 + */ + fun show(list: List) { + mAdapter.dataList.clear() + mAdapter.dataList.addAll(list) + openConversion(true) + super.show() + } + + /** + * 从字符串中查找子字符串位置列表 + */ + private fun findSubstringsIndexes(input: String?, searchString: String): ArrayList { + if (input.isNullOrEmpty()) { + return arrayListOf() + } + val indexes = arrayListOf() + var index = input.indexOf(searchString, 0) + while (index >= 0) { + indexes.add(index) + index = input.indexOf(searchString, index + 1) + } + return indexes + } + + /** + * 修改显示的搜索结果 + */ + @SuppressLint("SetTextI18n") + private fun changeShowSearchResult() { + binding.fcrOnlineEduRttConversionDialogSearchCount.text = "${if(mAdapter.getSumResultCount() == 0) "0" else (mAdapter.currentSearchResultIndex + 1)}/${mAdapter.getSumResultCount()}" + binding.fcrOnlineEduRttConversionDialogOptionsGoPre.isEnabled = mAdapter.currentSearchResultIndex > 0 + binding.fcrOnlineEduRttConversionDialogOptionsGoNext.isEnabled = mAdapter.currentSearchResultIndex < (mAdapter.getSumResultCount() - 1) + mAdapter.notifyItemRangeChanged(0, mAdapter.itemCount) + //滑动到指定位置 + var count = 0 + for (entry in mAdapter.searchResultListIndexMap) { + if (count <= mAdapter.currentSearchResultIndex && mAdapter.currentSearchResultIndex < count + entry.value.size) { + binding.fcrOnlineEduRttConversionDialogList.scrollToPosition(entry.key) + break + } + count += entry.value.size + } + + + } +} + +/** + * 转写操作接口类 + */ +interface ConversionOptionsInterface { + /** + * 开启转写 + */ + fun openConversion() + + /** + * 关闭转写 + */ + fun closeConversion() + + /** + * 打开设置页面 + */ + fun openSetting() +} + +/** + * 数据记录适配器 + */ +private class RecordAdapter(private val context: Context, val dataList: ArrayList) : RecyclerView.Adapter() { + /** + * 搜索结果下标 + * key值是列表中位置 + * value是数据元素位置 + */ + var searchResultListIndexMap = HashMap>() + + /** + * 搜索的文本 + */ + var searchText: String? = null + + /** + * 当前显示的查询结果位置 + */ + var currentSearchResultIndex = 0 + + /** + * 获取总的查询结果数量 + */ + fun getSumResultCount(): Int { + var count = 0 + for (entry in searchResultListIndexMap) { + count += entry.value.size + } + return count + } + + /** + * 获取当前条目之前的结果数量 + */ + fun getPreItemResultCount(position: Int): Int { + var count = 0 + for (entry in searchResultListIndexMap) { + if (entry.key < position) { + count += entry.value.size + } + } + return count + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if (dataList[viewType].statusText.isNullOrEmpty()) { + return object : RecyclerView.ViewHolder( + LayoutInflater.from(context).inflate(R.layout.fcr_online_rtt_conversion_dialog_list_content, parent, false)) {} + } + return object : + RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(R.layout.fcr_online_rtt_conversion_dialog_list_options, parent, false)) {} + } + + override fun getItemCount() = dataList.size + + override fun getItemViewType(position: Int): Int { + return position + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val bean = dataList[position] + if (bean.statusText.isNullOrEmpty()) { + holder.itemView.let { + Glide.with(context).load(bean.userHeader).skipMemoryCache(true).placeholder(R.drawable.agora_video_ic_audio_on) + .apply(RequestOptions.circleCropTransform()).into(it.findViewById(R.id.agora_fcr_rtt_text_dialog_user_header)) + it.findViewById(R.id.agora_fcr_rtt_text_dialog_user_header_text).text = + if (bean.userName.isNullOrEmpty()) "" else bean.userName!!.substring(0, 1) + it.findViewById(R.id.agora_fcr_rtt_text_dialog_user_name).text = bean.userName + it.findViewById(R.id.agora_fcr_rtt_text_dialog_time).text = + (if (bean.time == null || bean.time == 0L) null else bean.time)?.let { time -> Date(time) } + ?.let { date -> SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA).format(date) } + + val showText = bean.getShowText() + val level1Text = showText[0] + val level2Text = showText[1] + if (!searchResultListIndexMap.containsKey(position)) { + it.findViewById(R.id.agora_fcr_rtt_text_dialog_text_origin).text = level1Text + it.findViewById(R.id.agora_fcr_rtt_text_dialog_text_result).text = level2Text + } else { + val resultCount = getPreItemResultCount(position) + val sourceSpan = SpannableString(level1Text) + val targetTextSpan = if (level2Text.isNullOrEmpty()) null else SpannableString(level2Text) + searchResultListIndexMap[position]?.forEachIndexed { index, position -> + if (resultCount + index == currentSearchResultIndex) { + //当前是定位到的位置 + if (position < sourceSpan.length) { + sourceSpan.setSpan(ForegroundColorSpan(Color.WHITE), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + sourceSpan.setSpan(BackgroundColorSpan(Color.parseColor("#4262FF")), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + targetTextSpan?.setSpan(ForegroundColorSpan(Color.WHITE), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + targetTextSpan?.setSpan(BackgroundColorSpan(Color.parseColor("#4262FF")), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } else { + //非定位到的位置 + if (position < sourceSpan.length) { + sourceSpan.setSpan(BackgroundColorSpan(Color.parseColor("#334262FF")), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + targetTextSpan?.setSpan(BackgroundColorSpan(Color.parseColor("#334262FF")), position, position + searchText!!.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + + } + it.findViewById(R.id.agora_fcr_rtt_text_dialog_text_origin).text = sourceSpan + it.findViewById(R.id.agora_fcr_rtt_text_dialog_text_result).text = targetTextSpan + } + + it.findViewById(R.id.agora_fcr_rtt_text_dialog_text_result).apply { + visibility = if (RttLanguageEnum.NONE == bean.currentTargetLan) View.GONE else View.VISIBLE + } + } + } else { + holder.itemView.findViewById(R.id.fcr_online_rtt_conversion_dialog_list_options_text).text = bean.statusText + } + } +} \ No newline at end of file diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttSettingDialog.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttSettingDialog.kt new file mode 100644 index 000000000..aeda2ee03 --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/dialog/AgoraUIRttSettingDialog.kt @@ -0,0 +1,371 @@ +package io.agora.online.component.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.ColorDrawable +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.agora.online.R +import io.agora.online.databinding.FcrOnlineEduRttSettingDialogBinding +import io.agora.online.helper.RttSettingInfo + + +/** + * 功能作用:字幕、转写设置弹窗 + * 创建人:王亮(Loren) + * 思路: + * 方法: + * 注意: + * 修改人: + * 修改时间: + * 备注: + * + * @author 王亮(Loren) + */ +class AgoraUIRttSettingDialog(context: Context) : Dialog(context, R.style.agora_full_screen_dialog) { + + private val binding: FcrOnlineEduRttSettingDialogBinding by lazy { FcrOnlineEduRttSettingDialogBinding.inflate(layoutInflater, null, false) } + + /** + * 内容适配器 + */ + private val adapter = ContentAdapter(context) + + init { + setContentView(binding.root) + val window = this.window + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + window?.decorView?.setBackgroundResource(android.R.color.transparent) + val layout = findViewById(R.id.agora_dialog_layout) + layout.elevation = 10f + initView() + } + + private fun initView() { + binding.agoraFcrRttSettingDialogList.adapter = adapter + binding.agoraFcrRttSettingDialogList.layoutManager = LinearLayoutManager(context) + binding.agoraFcrRttSettingDialogClose.setOnClickListener { dismiss() } + } + + /** + * 显示设置弹窗 + */ + fun show(currentSettingInfo: RttSettingInfo) { + adapter.setConfigInfo(currentSettingInfo) + super.show() + } + + /** + * 设置监听 + */ + fun setListener(listener: AgoraUIRttSettingDialogListener) { + adapter.setListener(listener) + } + +} + +/** + * 设置弹窗监听 + */ +interface AgoraUIRttSettingDialogListener { + /** + * 修改双语显示 + */ + fun changeDoubleShow(showDouble: Boolean) + + /** + * 设置目标语言 + */ + fun setTargetLan(code: String) + + /** + * 设置声源语言 + */ + fun setSourceLan(code: String) +} + +/** + * 语言选择列表适配器 + */ +private class SelectListAdapter(var context: Context, var dataList: MutableList) : RecyclerView.Adapter() { + private var currentSelect: SelectItem? = null + private var lastSelect: SelectItem? = null + var onSelectChangedListener: OnSelectChangedListener? = null + + init { + currentSelect = dataList.find { it.select } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(context).inflate(R.layout.fcr_online_rtt_setting_dialog_list_select, parent, false)) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val item = dataList[position] + viewHolder.iconCheck.visibility = if (item.select) View.VISIBLE else View.GONE + viewHolder.iconDefault.visibility = if (!item.select) View.VISIBLE else View.GONE + viewHolder.itemView.setOnClickListener { + if (viewHolder.iconDefault.visibility == View.VISIBLE && item.allowSelect) { + lastSelect = if (lastSelect == null) { + dataList.find { it.select } + } else { + currentSelect + } + dataList.forEach { it.select = false } +// dataList.find { it.code == lastSelect!!.code }?.select = false + item.select = true + currentSelect = item + onSelectChangedListener?.onChanged(currentSelect!!) + notifyItemRangeChanged(0, itemCount) + } + } + viewHolder.title.text = dataList[position].text + } + + /** + * 重置到上一次 + */ + fun resetUseLast() { + if (lastSelect != null) { + currentSelect?.select = false + currentSelect = lastSelect + dataList.find { it.code == lastSelect!!.code }?.select = true + lastSelect = null + notifyItemRangeChanged(0, itemCount) + } + } + + override fun getItemCount() = dataList.size +} + +private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + var iconCheck: AppCompatImageView = view.findViewById(R.id.dialog_rtt_select_item_icon_checked) + var iconDefault: AppCompatImageView = view.findViewById(R.id.dialog_rtt_select_item_icon_default) + var title: AppCompatTextView = view.findViewById(R.id.dialog_rtt_select_item_title) +} + +/** + * 语音列表选择改变监听 + */ +private interface OnSelectChangedListener { + fun onChanged(select: SelectItem) +} + +/** + * 内容显示适配器 + */ +private class ContentAdapter(var context: Context) : RecyclerView.Adapter() { + + private val mFromAdapter by lazy { SelectListAdapter(context, mutableListOf()) } + private val mToAdapter by lazy { SelectListAdapter(context, mutableListOf()) } + + /** + * 配置信息 + */ + private var currentSettingInfo: RttSettingInfo? = null + + /** + * 监听 + */ + private var listener: AgoraUIRttSettingDialogListener? = null + + /** + * 内容的元素列表 + */ + private val contentConfigList = FcrRttSettingContentItemEnum.getShowConfigList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return object : RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(getLayout(viewType), parent, false)) {} + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + val itemEnum = contentConfigList[position] + when(itemEnum){ + FcrRttSettingContentItemEnum.SOURCE_LAN_TITLE -> { + (viewHolder.itemView as AppCompatTextView).setText(R.string.fcr_dialog_rtt_setting_dialog_title_origin) + } + FcrRttSettingContentItemEnum.SOURCE_LAN_LIST -> { + (viewHolder.itemView as RecyclerView).apply { + adapter = mFromAdapter + mFromAdapter.onSelectChangedListener = object : OnSelectChangedListener { + override fun onChanged(select: SelectItem) { + val useText = "“${select.text}”"//使用的变色文本 + val content = SpannableString( + String.format(resources.getString(R.string.fcr_dialog_rtt_setting_dialog_change_content), useText)) + content.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.fcr_blue_357BF6)), + content.indexOf(useText), content.indexOf(useText) + useText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + AgoraUIDialogBuilder(context).title(resources.getString(R.string.fcr_dialog_rtt_setting_dialog_change_title)) + .message(content).messagePaddingHorizontal(resources.getDimensionPixelOffset(R.dimen.dp_10)) + .negativeText(context.resources.getString(R.string.fcr_user_kick_out_cancel)).negativeClick { + mFromAdapter.resetUseLast() + }.positiveText(context.resources.getString(R.string.fcr_dialog_rtt_setting_dialog_change_confirm)).positiveClick { + listener?.setSourceLan(select.code) + + }.build().show() + } + } + layoutManager = LinearLayoutManager(context) + for (i in 0 until itemDecorationCount) { + removeItemDecorationAt(0) + } + addItemDecoration(ListSelectItemDecoration(this)) + } + } + FcrRttSettingContentItemEnum.TARGET_LAN_TITLE -> { + (viewHolder.itemView as AppCompatTextView).setText(R.string.fcr_dialog_rtt_setting_dialog_title_result) + } + FcrRttSettingContentItemEnum.TARGET_LAN_LIST -> { + (viewHolder.itemView as RecyclerView).apply { + adapter = mToAdapter + mToAdapter.onSelectChangedListener = object : OnSelectChangedListener { + override fun onChanged(select: SelectItem) { + listener?.setTargetLan(select.code) + } + } + layoutManager = LinearLayoutManager(context) + for (i in 0 until itemDecorationCount) { + removeItemDecorationAt(0) + } + addItemDecoration(ListSelectItemDecoration(this)) + } + } + FcrRttSettingContentItemEnum.DOUBLE_LAN_SHOW_SWITCH -> { + viewHolder.itemView.apply { + findViewById(R.id.fcr_online_rtt_setting_dialog_content_switch_title).setText( + R.string.fcr_dialog_rtt_setting_dialog_title_switch) + findViewById(R.id.fcr_online_rtt_setting_dialog_content_switch_icon).isActivated = + this@ContentAdapter.currentSettingInfo?.isShowDoubleLan() ?: false + setOnClickListener { + if (false == this@ContentAdapter.currentSettingInfo?.isShowDoubleLan()) { + findViewById(R.id.fcr_online_rtt_setting_dialog_content_switch_icon).isActivated = true + currentSettingInfo?.setShowDoubleLan(true) + listener?.changeDoubleShow(true) + } else { + findViewById(R.id.fcr_online_rtt_setting_dialog_content_switch_icon).isActivated = false + currentSettingInfo?.setShowDoubleLan(false) + listener?.changeDoubleShow(false) + } + } + } + } + } + } + + /** + * 获取布局类型 + */ + private fun getLayout(position: Int): Int { + return contentConfigList[position].layoutResId + } + + override fun getItemCount() = contentConfigList.size + + override fun getItemViewType(position: Int) = position + + /** + * 设置配置信息 + */ + fun setConfigInfo(currentSettingInfo: RttSettingInfo) { + this.currentSettingInfo = currentSettingInfo + mFromAdapter.dataList.clear() + for (item in currentSettingInfo.sourceListLan) { + mFromAdapter.dataList.add( + SelectItem(context.getString(item.textRes), item.value, true, item.value === currentSettingInfo.getSourceLan().value)) + } + mToAdapter.dataList.clear() + for (item in currentSettingInfo.targetListLan) { + mToAdapter.dataList.add( + SelectItem(context.getString(item.textRes), item.value, true, item.value == currentSettingInfo.getTargetLan().value )) + } + mFromAdapter.notifyItemRangeChanged(0, mFromAdapter.itemCount) + mToAdapter.notifyItemRangeChanged(0, mToAdapter.itemCount) + notifyItemRangeChanged(0, itemCount) + } + + /** + * 设置监听 + */ + fun setListener(listener: AgoraUIRttSettingDialogListener) { + this.listener = listener + } +} + +/** + * 选项列表分割线 + */ +private class ListSelectItemDecoration(private val recyclerView: RecyclerView) : RecyclerView.ItemDecoration() { + private val paint = Paint().apply { + this.isAntiAlias = true + this.color = ContextCompat.getColor(recyclerView.context, R.color.fcr_im_input_bg_line) + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val childCount = parent.childCount + for (i in 0 until childCount - 1) { // childCount - 1 to avoid drawing a divider after the last item + val child = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + + val top = child.bottom + params.bottomMargin + c.drawRect(recyclerView.paddingLeft.toFloat(), top.toFloat(), recyclerView.right.toFloat(), + (top + recyclerView.resources.getDimensionPixelSize(R.dimen.dp_1)).toFloat(), paint) + } + } +} + +/** + * 选项 + */ +private class SelectItem(val text: String, val code: String, val allowSelect: Boolean, var select: Boolean) + +/** + * Rtt设置内容弹窗的item枚举 + */ +private enum class FcrRttSettingContentItemEnum(@LayoutRes val layoutResId: Int) { + /** + * 声源语言标题 + */ + SOURCE_LAN_TITLE(R.layout.fcr_online_rtt_setting_dialog_content_title), + + /** + * 声源语言列表 + */ + SOURCE_LAN_LIST(R.layout.fcr_online_rtt_setting_dialog_content_list), + + /** + * 翻译语言标题 + */ + TARGET_LAN_TITLE(R.layout.fcr_online_rtt_setting_dialog_content_title), + + /** + * 翻译语言列表 + */ + TARGET_LAN_LIST(R.layout.fcr_online_rtt_setting_dialog_content_list), + + /** + * 是否显示双语 + */ + DOUBLE_LAN_SHOW_SWITCH(R.layout.fcr_online_rtt_setting_dialog_content_switch); + + companion object { + fun getShowConfigList(): List { + return listOf(SOURCE_LAN_TITLE, SOURCE_LAN_LIST, TARGET_LAN_TITLE, TARGET_LAN_LIST, DOUBLE_LAN_SHOW_SWITCH) + } + } +} diff --git a/AgoraCloudScene/src/main/java/io/agora/online/component/toast/AgoraUIToast.kt b/AgoraCloudScene/src/main/java/io/agora/online/component/toast/AgoraUIToast.kt index 3dd4415d7..68920b44f 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/component/toast/AgoraUIToast.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/component/toast/AgoraUIToast.kt @@ -6,6 +6,7 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.os.Handler +import android.text.SpannableString import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -24,6 +25,7 @@ object AgoraUIToast { private const val LEVEL_INFO = 1 private const val LEVEL_WARN = 2 private const val LEVEL_ERROR = 3 + private const val LEVEL_NONE = 4 // private var COLOR_INFO = Color.parseColor("#FAFFFF") // private var COLOR_WARN = Color.parseColor("#FFFBF4") @@ -118,21 +120,49 @@ object AgoraUIToast { showToast(context.applicationContext, LEVEL_ERROR, anchor, text, duration) } + /** + * 显示默认弹窗 + */ + fun showDefaultToast(context: Context, message: String, duration: Int = LENGTH_SHORT) { + showDefaultToast(context, SpannableString(message), duration) + } + + /** + * 显示默认弹窗 + */ + fun showDefaultToast(context: Context, message: SpannableString, duration: Int = LENGTH_SHORT) { + ContextCompat.getMainExecutor(context).execute { + computeValues() + val toastLayout = LayoutInflater.from(context).inflate(R.layout.fcr_online_toast_layout_default, null, false) + toastLayout.findViewById(R.id.tvText)?.let { msgView -> + msgView.text = message + } + val toast = Toast(context.applicationContext) + toast.view = toastLayout + toast.duration = duration + toast.setGravity(Gravity.TOP, 0, 200) + toast.show() + } + } + /** * Display the toast below the target anchor view * @param anchor tells how to position this toast on the screen, * if null, display the toast at the system default position */ @SuppressLint("InflateParams") - private fun showToast(context: Context, level: Int, anchor: View?, text: String, duration: Int) { + private fun showToast(context: Context, level: Int, anchor: View?, text: CharSequence, duration: Int) { ContextCompat.getMainExecutor(context).execute { computeValues() - val toastLayout = LayoutInflater.from(context).inflate( - R.layout.fcr_online_toast_layout, null, false) + val toastLayout = LayoutInflater.from(context).inflate(R.layout.fcr_online_toast_layout, null, false) toastLayout.findViewById(R.id.agora_toast_icon)?.let { icon -> - getToastIconRes(level)?.let { iconRes -> - icon.setImageResource(iconRes) + val res = getToastIconRes(level) + if(res !=null){ + icon.visibility = View.VISIBLE + icon.setImageResource(res) + }else{ + icon.visibility = View.GONE } } @@ -157,26 +187,30 @@ object AgoraUIToast { } } + private fun computeValues() { } - private fun buildToastBgDrawable(context: Context,level: Int): Drawable? { + private fun buildToastBgDrawable(context: Context, level: Int): Drawable? { val bgColor: Int val borderColor: Int when (level) { LEVEL_INFO -> { - bgColor = ContextCompat.getColor(context,R.color.fcr_system_safe_color) + bgColor = ContextCompat.getColor(context, R.color.fcr_system_safe_color) borderColor = COLOR_INFO_BORDER } + LEVEL_WARN -> { - bgColor = ContextCompat.getColor(context,R.color.fcr_system_warning_color) + bgColor = ContextCompat.getColor(context, R.color.fcr_system_warning_color) borderColor = COLOR_WARN_BORDER } + LEVEL_ERROR -> { - bgColor = ContextCompat.getColor(context,R.color.fcr_system_error_color) + bgColor = ContextCompat.getColor(context, R.color.fcr_system_error_color) borderColor = COLOR_ERROR_BORDER } + else -> { return null } diff --git a/AgoraCloudScene/src/main/java/io/agora/online/helper/FcrRttOptionsManager.kt b/AgoraCloudScene/src/main/java/io/agora/online/helper/FcrRttOptionsManager.kt new file mode 100644 index 000000000..d539d1a7e --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/helper/FcrRttOptionsManager.kt @@ -0,0 +1,1915 @@ +package io.agora.online.helper + +import android.content.Context +import android.os.Handler +import android.os.Message +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import io.agora.agoraeducore.core.AgoraEduCore +import io.agora.agoraeducore.core.context.AgoraEduContextUserInfo +import io.agora.agoraeducore.core.context.EduContextUserLeftReason +import io.agora.agoraeducore.core.context.StreamContext +import io.agora.agoraeducore.core.internal.base.http.AppRetrofitManager +import io.agora.agoraeducore.core.internal.education.impl.network.HttpBaseRes +import io.agora.agoraeducore.core.internal.education.impl.network.HttpCallback +import io.agora.agoraeducore.core.internal.framework.data.EduBaseUserInfo +import io.agora.agoraeducore.core.internal.framework.data.EduUserRole +import io.agora.agoraeducore.core.internal.framework.impl.handler.RoomHandler +import io.agora.agoraeducore.core.internal.framework.impl.handler.StreamHandler +import io.agora.agoraeducore.core.internal.framework.impl.handler.UserHandler +import io.agora.agoraeducore.core.internal.framework.utils.GsonUtil +import io.agora.agoraeducore.core.internal.log.LogX +import io.agora.online.R +import io.agora.online.component.common.IAgoraUIProvider +import io.agora.online.component.dialog.AgoraUIDialogBuilder +import io.agora.online.component.dialog.AgoraUIRttConversionDialogBuilder +import io.agora.online.component.dialog.AgoraUIRttSettingBuilder +import io.agora.online.component.dialog.AgoraUIRttSettingDialogListener +import io.agora.online.component.dialog.ConversionOptionsInterface +import io.agora.online.component.toast.AgoraUIToast +import io.agora.online.easeim.utils.TAG +import io.agora.online.easeim.view.ui.widget.RoomType +import io.agora.online.util.MultiLanguageUtil +import io.agora.online.util.SpUtil +import io.agora.rtc.speech2text.AgoraSpeech2TextProtobuffer +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.PUT +import retrofit2.http.Path +import java.util.Locale +import java.util.UUID + +/** + * 功能作用:Rtt相关操作管理 + * 创建人:王亮(Loren) + * 思路: + * 方法: + * 注意: + * 修改人: + * 修改时间: + * 备注: + * + * 需求: + * 如有人正在说话,字母区域提示:listening(正在聆听) ... + * 如没有人说话,提示:no one is currently speaking(当前没有人在说话) + * 房间内有音频输出时,字幕区域显示对应转写文字,无音频输出时,字幕区域在音频停止3S后自动消失 + * + * 场景: + * 音频流结束&翻译未结束 + * 音频流未结束&翻译未结束 + * 音频流未结束&上一次翻译结束 + * 音频流结束&当前翻译结束 + * + * + * 实现逻辑: + * + * + * + * @author 王亮(Loren) + */ +class RttOptionsManager(internal val rttOptions: IRttOptions) { + private val TAG = "RttOptionsManager:" + + /** + * 教室相关信息 + */ + internal var eduCore: AgoraEduCore? = null + + private val listenerList = arrayListOf() + + /** + * 转写管理 + */ + private val conversionManager by lazy { + RttConversionManager(this, this.getManagerListener()) + } + + /** + * 字幕管理 + */ + private val subtitlesManager by lazy { RttSubtitlesManager(this, getManagerListener()) } + + /** + * 设置管理 + */ + private val settingsManager by lazy { RttSettingManager(this) } + + /** + * 使用管理 + */ + private val useManager by lazy { RttUseManager(this, false, getManagerListener()) } + + /** + * 流回调监听 + */ + private val steamHandler = object : StreamHandler() { + override fun onStreamMessage(channelId: String, streamId: Int, data: ByteArray?) { + super.onStreamMessage(channelId, streamId, data) + //不允许使用或者已到体验时间就不在回调,在分组中也不对消息数据进行处理 + if (!isAllowUseRtt() || (!conversionManager.isOpenConversion() && !subtitlesManager.isOpenSubtitles()) || checkUserInSubRoom()) { + return + } + val parseFrom = AgoraSpeech2TextProtobuffer.Text.parseFrom(data) + val recordItem = useManager.disposeData(parseFrom, settingsManager.currentSettingInfo, eduCore?.eduContextPool()?.streamContext()) + subtitlesManager.setShowCurrentData(recordItem, settingsManager.currentSettingInfo) + conversionManager.updateShowList(useManager.getShowRecordList()) + LogX.i(TAG, "onStreamMessage channelId=$channelId, streamId=$streamId, data=${GsonUtil.toJson(parseFrom)}") + + } + + override fun onStreamMessageError( + channelId: String, + streamId: Int, + error: Int, + missed: Int, + cached: Int, + ) { + super.onStreamMessageError(channelId, streamId, error, missed, cached) + LogX.i(TAG, "onStreamMessageError channelId=$channelId, streamId=$streamId, error=$error, missed=$missed, cached=$cached") + } + } + + /** + * 房间回调监听 + */ + private val roomHandler = object : RoomHandler() { + override fun onRoomPropertiesUpdated(properties: Map, cause: Map?, operator: AgoraEduContextUserInfo?) { + super.onRoomPropertiesUpdated(properties, cause, operator) + LogX.i(TAG, "onRoomPropertiesUpdated properties=${GsonUtil.toJson(properties)}, cause=${ + cause?.let { GsonUtil.toJson(it) } + }, operator=${operator?.let { GsonUtil.toJson(it) }}" + ) + } + } + + /** + * 用户监听 + */ + private val userHandler = object : UserHandler() { + override fun onRemoteUserLeft(user: AgoraEduContextUserInfo, operator: AgoraEduContextUserInfo?, reason: EduContextUserLeftReason) { + super.onRemoteUserLeft(user, operator, reason) + useManager.formatAllUserInfo(eduCore?.eduContextPool()?.streamContext()) + } + + override fun onRemoteUserJoined(user: AgoraEduContextUserInfo) { + super.onRemoteUserJoined(user) + useManager.formatAllUserInfo(eduCore?.eduContextPool()?.streamContext()) + } + } + + init { + AgoraUIToast.init(rttOptions.getApplicationContext()) + } + + /** + * 获取管理器监听 + */ + private fun getManagerListener(): FcrRttOptionsStatusListener { + return object : FcrRttOptionsStatusListener() { + /** + * rtt功能状态变更 + * @param open 开启-true,关闭-false + */ + override fun rttStateChange(open: Boolean) { + listenerList.forEach { it.rttStateChange(open) } + } + + /** + * 音频状态-无人讲话 + */ + override fun audioStateNoSpeaking() { + listenerList.forEach { it.audioStateNoSpeaking() } + } + + /** + * 音频状态-有人讲话 + */ + override fun audioStateSpeaking() { + listenerList.forEach { it.audioStateSpeaking() } + } + + /** + * 音频状态-超过一定时间无人讲话 + */ + override fun audioStateNoSpeakingMoreTime() { + listenerList.forEach { it.audioStateNoSpeakingMoreTime() } + } + + /** + * 音频状态-开启中 + */ + override fun audioStateOpening() { + listenerList.forEach { it.audioStateOpening() } + } + + /** + * 音频状态-无法使用 + */ + override fun audioStateNotAllowUse() { + listenerList.forEach { it.audioStateNotAllowUse() } + } + + /** + * 音频状态-显示设置提示 + */ + override fun audioStateShowSettingHint() { + listenerList.forEach { it.audioStateShowSettingHint() } + } + + /** + * 体验信息变更 + * @param configAllowUseRtt 配置是否可以使用rtt功能 + * @param experienceReduceTime 剩余体验时间 + * @param experienceDefaultTime 默认体验时间 + */ + override fun experienceInfoChange(configAllowUseRtt: Boolean, experienceDefaultTime: Int, experienceReduceTime: Int) { + conversionManager.setExperienceInfo(configAllowUseRtt, experienceReduceTime) + listenerList.forEach { it.experienceInfoChange(configAllowUseRtt, experienceDefaultTime, experienceReduceTime) } + } + + /** + * 字幕状态变更 + * @param toOpen 开启-true,关闭-false + */ + override fun subtitlesStateChange(toOpen: Boolean) { + listenerList.forEach { it.subtitlesStateChange(toOpen) } + } + + /** + * 字幕视图重置 + */ + override fun subtitlesViewReset(openSuccess: Boolean) { + listenerList.forEach { it.subtitlesViewReset(openSuccess) } + } + + /** + * 转写状态变更 + * @param toOpen 开启-true,关闭-false + */ + override fun conversionStateChange(toOpen: Boolean) { + if (conversionManager.isOpenConversion() == toOpen) { + return + } + listenerList.forEach { it.conversionStateChange(toOpen) } + } + + /** + * 转写视图重置 + */ + override fun conversionViewReset() { + listenerList.forEach { it.conversionViewReset() } + } + + /** + * 声源语言修改 + */ + override fun sourceLanguageChange(language: RttLanguageEnum) { + listenerList.forEach { it.sourceLanguageChange(language) } + } + + /** + * 目标语言修改-网络请求结果 + */ + override fun targetLanguageChange(languages: List) { + listenerList.forEach { it.targetLanguageChange(languages) } + } + + /** + * 双语状态变更 + * @param open 开启-true,关闭-false + */ + override fun showDoubleLanguage(open: Boolean) { + listenerList.forEach { it.showDoubleLanguage(open) } + } + + /** + * 双语状态变更-网络请求结果 + * @param open 开启-true,关闭-false + */ + override fun showDoubleLanguageNetResult(open: Boolean) { + listenerList.forEach { it.showDoubleLanguageNetResult(open) } + } + + /** + * 消息改变 + * @param currentData 当前要显示的数据 + */ + override fun onMessageChange(currentData: RttRecordItem?) { + listenerList.forEach { it.onMessageChange(currentData) } + } + } + } + + /** + * 检测用户是否在分组中 + */ + fun checkUserInSubRoom(): Boolean { + return eduCore?.eduContextPool()?.roomContext()?.getRoomInfo()?.roomType?.value == RoomType.GROUPING_CLASS.value + } + + /** + * 新增监听 + */ + fun addListener(listener: FcrRttOptionsStatusListener) { + if (!listenerList.contains(listener)) { + listenerList.add(listener) + } + } + + /** + * 移除监听 + */ + fun removeListener(listener: FcrRttOptionsStatusListener) { + if (listenerList.contains(listener)) { + listenerList.remove(listener) + } + } + + /** + * 初始化视图 + * @param agoraUIProvider UI配置信息 + */ + fun initView(agoraUIProvider: IAgoraUIProvider) { + this.eduCore = agoraUIProvider.getAgoraEduCore() + //重置视图 + this.setRttFunctionStatusConfig() + //添加监听 + this.eduCore?.eduContextPool()?.streamContext()?.addHandler(steamHandler) + this.eduCore?.eduContextPool()?.roomContext()?.addHandler(roomHandler) + this.eduCore?.eduContextPool()?.userContext()?.addHandler(userHandler) + } + + /** + * 是否开启了字幕 + */ + fun isOpenSubtitles(): Boolean { + return subtitlesManager.isOpenSubtitles() + } + + /** + * 是否显示双语 + */ + fun isShowDoubleLan(): Boolean { + return settingsManager.currentSettingInfo.isShowDoubleLan() + } + + /** + * 是否开启了转写 + */ + fun isOpenConversion(): Boolean { + return conversionManager.isOpenConversion() + } + + /** + * 是否允许使用Rtt功能 + */ + fun isAllowUseRtt(): Boolean { + return useManager.isAllowUse() + } + + /** + * 设置RTT功能状态配置 + * @param allowUse 是否允许使用rtt功能,不可使用的话则提供体验时间 + */ + private fun setRttFunctionStatusConfig(allowUse: Boolean = false) { + useManager.configAllowUse = allowUse + if (!allowUse) { + this.resetShow() + } + } + + /** + * 释放相关 + */ + fun release() { + listenerList.forEach { it.conversionViewReset();it.subtitlesViewReset(false) } + this.eduCore?.eduContextPool()?.streamContext()?.removeHandler(steamHandler) + this.eduCore?.eduContextPool()?.roomContext()?.removeHandler(roomHandler) + this.eduCore?.eduContextPool()?.userContext()?.removeHandler(userHandler) + listenerList.clear() + } + + /** + * 开启字幕 + */ + fun openSubtitles() { + subtitlesManager.openSubtitles(useManager.configAllowUse, useManager.rttExperienceDefaultTime, useManager.rttExperienceReduceTime) + } + + /** + * 关闭字幕 + */ + fun closeSubtitles() { + subtitlesManager.closeSubtitles() + } + + /** + * 开启转写 + */ + fun openConversion() { + this.localIsChangeTranscribeState = true + conversionManager.openConversion(useManager.getShowRecordList()) + } + + /** + * 关闭转写 + */ + fun closeConversion() { + conversionManager.closeConversion() + } + + /** + * 开启设置 + */ + fun openSetting() { + this.settingsManager.openSetting() + } + + /** + * 关闭设置 + */ + fun closeSetting() { + this.settingsManager.closeSetting() + } + + /** + * 开启体验 + */ + fun startExperience() { + this.useManager.startExperience() + } + + /** + * 关闭体验 + */ + fun stopExperience() { + this.useManager.stopExperience() + } + + /** + * 设置转换源语言 + */ + fun setSourceLanguage(lan: RttLanguageEnum) { + settingsManager.currentSettingInfo.setSourceLan(lan) + this.localIsChangeSourceLan = true + sendRequest(isOpenConversion(), isOpenSubtitles(), object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + useManager.setLastFinal() + this@RttOptionsManager.getManagerListener().sourceLanguageChange(lan) + } + }) + } + + /** + * 设置转换目标语言 + */ + fun setTargetLanguage(lan: RttLanguageEnum) { + settingsManager.currentSettingInfo.setTargetLan(lan) + this.localIsChangeTarget = true + sendRequest(isOpenConversion(), isOpenSubtitles(), object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + useManager.setLastFinal() + this@RttOptionsManager.getManagerListener().targetLanguageChange(settingsManager.currentSettingInfo.getTargetLanList()) + } + }) + } + + /** + * 硒鼓双语显示 + */ + fun changeDoubleShow(showDouble: Boolean) { + settingsManager.currentSettingInfo.setShowDoubleLan(showDouble) + } + + /** + * 获取体验剩余时间 + */ + fun getExperienceReduceTime(): Int { + return useManager.rttExperienceReduceTime + } + + /** + * 获取体验默认时间 + */ + fun getExperienceDefaultTime(): Int { + return useManager.rttExperienceDefaultTime + } + + /** + * 设置语言列表 + */ + fun setSourceListLanguages(list: ArrayList) { + this.settingsManager.currentSettingInfo.sourceListLan.clear() + this.settingsManager.currentSettingInfo.sourceListLan.addAll(list) + this.settingsManager.currentSettingInfo.sourceListLan.distinct() + } + + /** + * 新增语言列表 + */ + fun setTargetListLanguages(list: ArrayList) { + this.settingsManager.currentSettingInfo.targetListLan.clear() + this.settingsManager.currentSettingInfo.targetListLan.addAll(list) + this.settingsManager.currentSettingInfo.sourceListLan.distinct() + } + + //是否正在发起请求 + private var isSendRequesting = false + + /** + * 发起请求 + * @param openRtt 是否打开rtt功能 + * @param openRttSubtitles 是否开启字幕 + */ + internal fun sendRequest( + openRttConversion: Boolean, openRttSubtitles: Boolean, + callback: HttpCallback>? = null, + ) { + if (isSendRequesting) { + return + } + isSendRequesting = true + val body = FcrRttChangeOptionsData.formatUseData(openRttConversion, openRttSubtitles, settingsManager.currentSettingInfo.getSourceLan().value, + if (openRttConversion || openRttSubtitles) settingsManager.currentSettingInfo.getTargetLanList().filter { "" != it.value }.map { it.value }.distinct().toTypedArray() else arrayOf() + ) + val call = AppRetrofitManager.instance().getService(IRttOptionsService::class.java) + .buildTokens(eduCore?.config?.appId, eduCore?.config?.roomUuid, if (openRttConversion || openRttSubtitles) 1 else 0, body) + AppRetrofitManager.exc(call, object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + useManager.setExperienceReduceTime(useManager.rttExperienceDefaultTime - ((result?.data?.duration ?: 0) * 1000)) + callback?.onSuccess(result) + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + if (httpCode == 409) { + //已经开始了转写翻译任务 + callback?.onSuccess(null) + return + } + callback?.onError(httpCode, code, message) + } + + override fun onComplete() { + super.onComplete() + isSendRequesting = false + } + }) + } + + /** + * 重置显示 + */ + private fun resetShow() { + listenerList.forEach { it.conversionViewReset();it.subtitlesViewReset(false) } + } + + /** + * 本地是否修改了声源语言,因为未返回实际的修改字段,所以需要做首次获取的更新处理,因为首次开启关闭会触发这个更新,但是实际上声源不一定会有修改 + */ + private var localIsChangeSourceLan = false + + /** + * 本地是否修改了转写状态,原因同上 + */ + private var localIsChangeTranscribeState = false + + /** + * 本地是否修改了翻译状态,原因同上 + */ + private var localIsChangeTarget = false + + /** + * 房间widget属性更新 + */ + fun onWidgetRoomPropertiesUpdated(properties: MutableMap, operator: EduBaseUserInfo?) { + if ("server" != operator?.userUuid) { + GsonUtil.jsonToObject(GsonUtil.toJson(properties))?.let { + operator?.let { userInfo -> + LogX.i(TAG, "onWidgetRoomPropertiesUpdated result=${GsonUtil.toJson(properties)}__user=${GsonUtil.toJson(operator)}") + //剩余体验时间 + useManager.setExperienceReduceTime(useManager.rttExperienceDefaultTime - (it.duration * 1000)) + //用户信息 + val localUserInfo = eduCore?.eduContextPool()?.userContext()?.getLocalUserInfo() + //判断是否改变了转写状态 + if (checkUpdateConversion(it) || localIsChangeTranscribeState && operator.userUuid != localUserInfo?.userUuid) { + this.localIsChangeTranscribeState = false + conversionManager.addStateChangeTextMessage(it.transcribe, userInfo, localUserInfo, useManager) + conversionManager.initOpenConversion(1 == it.transcribe) + } + //判断是否开启了翻译(仅当自己生效) + if (localIsChangeTarget && operator.userUuid == localUserInfo?.userUuid && !it.languages?.target.isNullOrEmpty() && this.localIsChangeTarget) { + this.localIsChangeTarget = false + settingsManager.currentSettingInfo.setTargetLanList( + it.languages?.target?.map { mapItem -> RttLanguageEnum.values().find { it.value == mapItem }!! }?.toTypedArray() + ?: arrayOf() + ) + conversionManager.addTargetLanguageChangeMessage(true, userInfo, useManager) + } + //判断是否修改了声源语言 + if (!it.languages?.source.isNullOrEmpty() && it.languages?.source != settingsManager.currentSettingInfo.getSourceLan().value || localIsChangeSourceLan && operator.userUuid == localUserInfo?.userUuid) { + this.localIsChangeSourceLan = false + val sourceEnum = RttLanguageEnum.values().find { item -> item.value == it.languages?.source } + if (sourceEnum != null) { + settingsManager.currentSettingInfo.setSourceLan(sourceEnum) + conversionManager.addSourceLanguageChangeMessage(userInfo, localUserInfo, useManager, settingsManager, sourceEnum) + } + } + + //是否开启字幕 + if (0 == it.subtitle && this.isOpenSubtitles()) { + //重新发起字幕开启 + sendRequest(this.isOpenConversion(), openRttSubtitles = this.isOpenSubtitles(), + callback = object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + } + }) + } + } + } + } + } + + /** + * 初始化widget属性 + */ + fun onWidgetRoomPropertiesInit(properties: MutableMap?) { + if (properties != null) { + GsonUtil.jsonToObject(GsonUtil.toJson(properties))?.let { + LogX.i(TAG, "onWidgetRoomPropertiesInit result=${GsonUtil.toJson(properties)}}") + //剩余体验时间 + useManager.setExperienceReduceTime(useManager.rttExperienceDefaultTime - (it.duration * 1000)) + //声源语言 + val sourceEnum = RttLanguageEnum.values().find { item -> item.value == it.languages?.source } + if (sourceEnum != null) { + settingsManager.currentSettingInfo.setSourceLan(sourceEnum) + } + } + } + } + + /** + * 检测是否更新了转写状态 + */ + private fun checkUpdateConversion(it: FcrRttChangeOptionsData): Boolean { + if (1 == it.transcribe) { + if (!conversionManager.isOpenConversion()) { + return true + } + } else if (0 == it.transcribe) { + if (conversionManager.isOpenConversion()) { + return true + } + } + return false + } + + /** + * 格式化名称角色显示 + * @param optionsUser 发起设置修改的用户信息 + * @param localUser 当前本地的用户信息 + */ + fun formatRoleName(optionsUser: EduBaseUserInfo, localUser: AgoraEduContextUserInfo?): String { + return "${ + if (EduUserRole.STUDENT == optionsUser.role) { + rttOptions.getApplicationContext().getString(R.string.fcr_role_student) + } else if (EduUserRole.TEACHER == optionsUser.role) { + rttOptions.getApplicationContext().getString(R.string.fcr_role_teacher) + } else { + "" + } + }(${ + if (optionsUser.userUuid == localUser?.userUuid) { + rttOptions.getApplicationContext().getString(R.string.fcr_dialog_rtt_text_role_me) + } else { + optionsUser.userName + } + }) " + } + + /** + * 体验时间是否用完 + */ + fun experienceReduceTimeZero(): Boolean { + return useManager.experienceReduceTimeZero() + } +} + +/** + * rtt操作接口 + */ +interface IRttOptions { + /** + * 获取应用实例 + */ + fun getApplicationContext(): Context + + /** + * 当前页面实例 + */ + fun getActivityContext(): Context + + /** + * 判断并切换主线程 + */ + fun runOnUiThread(runnable: Runnable) +} + +/** + * 接口中请求的语言配置信息 + */ +internal class RttChangOptionsLanguage(val source: String, targetList: Array) { + val target: Array = targetList.filter { it.isNotEmpty() }.toTypedArray() +} + +/** + * 接口请求的语言配置信息响应 + */ +internal data class RttChangeOptionsRes( + /** + * 已用时间 + */ + val duration: Int? = null, + /** + * 语言配置 + */ + val languages: RttChangOptionsLanguage? = null, + /** + * 开始时间 + */ + val startTime: Long? = null, + /** + * 1-开启rtt,0-关闭rtt + */ + val state: Int? = null, + /** + * 流Id + */ + val streamUuid: String? = null, + /** + * 1-开启了字幕,0-关闭了字幕 + */ + val subtitle: Int? = null, + /** + * 任务id + */ + val taskId: String? = null, + /** + * token信息 + */ + val token: String? = null, + /** + * 1-开启了转写,0-关闭了转写 + */ + val transcribe: Int? = null, +) + +/** + * RTT转写记录 + */ +class RttRecordItemTran { + /** + * 目标语言 + */ + var language: RttLanguageEnum? = null + + /** + * 文案 + */ + var text: String? = null +} + +/** + * RTT记录 + */ +class RttRecordItem { + /** + * 转写和字幕翻译之外的状态文本 + */ + var statusText: String? = null + + /** + * 唯一id + */ + var uuid: String? = null + + /** + * 是否显示双语 + */ + var showDoubleLan: Boolean = false + + /** + * 语言信息 + */ + var sourceLan: RttLanguageEnum? = null + + /** + * 当前翻译的目标语言 + */ + var currentTargetLan: RttLanguageEnum? = null + + /** + * 置信度 + */ + var sourceConfidence: Double? = null + + /** + * 转写内容对应的Rtc uid + */ + var uid: Long? = null + + /** + * 说话的用户昵称 + */ + var userName: String? = null + + /** + * 说话的用户头像 + */ + var userHeader: String? = null + + /** + * 源文案 + */ + var sourceText: String? = null + + /** + * 目标文案 + */ + var targetText: String? = null + + /** + * 目标信息 + */ + var targetInfo: ArrayList? = null + + /** + * 时间 + */ + var time: Long? = null + + /** + * 是否是最终结果 + */ + var isFinal: Boolean = false + + + /** + * 获取显示的源文本信息 + * @returns + */ + fun getShowText(): Array { + //是否设置了翻译语言 + val enableTargetLan = "" != currentTargetLan?.value + //语言显示 + val leve2Text = + if (showDoubleLan && enableTargetLan && currentTargetLan != null && sourceLan != null && sourceLan!!.value != currentTargetLan?.value) targetText else null + val leve1Text = if (!showDoubleLan && enableTargetLan && !targetText.isNullOrEmpty()) targetText else sourceText + return arrayOf(leve1Text, leve2Text) + } + +} + +/** + * 当前语言配置信息 + * @param sourceListLan 可用的源语言列表 + * @param targetListLan 可用的目标语言列表 + * @param sourceLan 转换的源语言 + * @param targetLan 转换的目标语言 + * @param showDoubleLan 是否显示双语 + */ +class RttSettingInfo( + var rttOptionsManager: RttOptionsManager, + val sourceListLan: ArrayList = arrayListOf(RttLanguageEnum.ZH_CN, RttLanguageEnum.EN_US, RttLanguageEnum.JA_JP), + val targetListLan: ArrayList = arrayListOf( + RttLanguageEnum.NONE, RttLanguageEnum.ZH_CN, RttLanguageEnum.EN_US, + RttLanguageEnum.JA_JP + ), + private var sourceLan: RttLanguageEnum = RttLanguageEnum.ZH_CN, + private var targetLanList: ArrayList = arrayListOf(RttLanguageEnum.NONE), + private var targetLan: RttLanguageEnum = RttLanguageEnum.NONE, + private var showDoubleLan: Boolean = SpUtil.getBoolean( + rttOptionsManager.rttOptions.getApplicationContext(), + "${rttOptionsManager.eduCore?.config?.roomUuid ?: ""}_showDoubleLan", false + ), +) { + init { + val locale: Locale = MultiLanguageUtil.getAppLocale(rttOptionsManager.rttOptions.getActivityContext()) + // zh_CN || zh_TW + val sysLanguage = locale.language // zh + val sysCountry = locale.country // CN + //默认源语言设置 + sourceLan = + if (sysLanguage.equals(Locale.SIMPLIFIED_CHINESE.language, ignoreCase = true) && sysCountry.equals( + Locale.SIMPLIFIED_CHINESE.country, + ignoreCase = true + ) + ) { + RttLanguageEnum.ZH_CN + } else if (sysLanguage.equals(Locale.US.language, ignoreCase = true) && sysCountry.equals(Locale.US.country, ignoreCase = true)) { + RttLanguageEnum.EN_US + } else if (Locale.getDefault().language.equals( + Locale.SIMPLIFIED_CHINESE.language, + ignoreCase = true + ) && Locale.getDefault().country.equals(Locale.SIMPLIFIED_CHINESE.country, ignoreCase = true) + ) { + RttLanguageEnum.ZH_CN + } else if (Locale.getDefault().language.equals(Locale.US.language, ignoreCase = true) && Locale.getDefault().country.equals( + Locale.US.country, ignoreCase = true + ) + ) { + RttLanguageEnum.EN_US + } else { + RttLanguageEnum.EN_US + } + } + + fun getSourceLan() = sourceLan + fun getTargetLanList() = targetLanList + fun getTargetLan() = targetLan + fun isShowDoubleLan() = showDoubleLan + fun setSourceLan(sourceLan: RttLanguageEnum) { + this.sourceLan = sourceLan + } + + fun setTargetLan(targetLan: RttLanguageEnum) { + this.targetLan = targetLan + this.targetLanList.add(targetLan) + } + + fun setTargetLanList(targetLan: Array) { + this.targetLanList = targetLan.toCollection(arrayListOf()) + } + + fun setShowDoubleLan(show: Boolean) { + this.showDoubleLan = show + SpUtil.saveBoolean( + rttOptionsManager.rttOptions.getApplicationContext(), "${rttOptionsManager.eduCore?.config?.roomUuid ?: ""}_showDoubleLan", + show + ) + } +} + +/** + * RTT可用语言枚举 + */ +enum class RttLanguageEnum(@StringRes val textRes: Int, val value: String) { + /** + * 不翻译 + */ + NONE(R.string.fcr_dialog_rtt_language_none, ""), + + /** + * Chinese (Cantonese, Traditional) + */ + ZH_HK(R.string.fcr_dialog_rtt_language_zh_hk, "zh-HK"), + + /** + * Chinese (Mandarin, Simplified) + */ + ZH_CN(R.string.fcr_dialog_rtt_language_zh_cn, "zh-CN"), + + /** + * Chinese (Taiwanese Putonghua) + */ + ZH_TW(R.string.fcr_dialog_rtt_language_zh_tw, "zh-TW"), + + /** + * English (India) + */ + EN_IN(R.string.fcr_dialog_rtt_language_en_in, "en-IN"), + + /** + * English (US) + */ + EN_US(R.string.fcr_dialog_rtt_language_en_us, "en-US"), + + /** + * French (French) + */ + FR_FR(R.string.fcr_dialog_rtt_language_fr_fr, "fr-FR"), + + /** + * German (Germany) + */ + DE_DE(R.string.fcr_dialog_rtt_language_de_de, "de-DE"), + + /** + * Hai (Thailand) + */ + TH_TH(R.string.fcr_dialog_rtt_language_th_th, "th-TH"), + + /** + * Hindi (India) + */ + HI_IN(R.string.fcr_dialog_rtt_language_hi_in, "hi-IN"), + + /** + * Indonesian (Indonesia) + */ + ID_ID(R.string.fcr_dialog_rtt_language_id_id, "id-ID"), + + /** + * Italian (Italy) + */ + IT_IT(R.string.fcr_dialog_rtt_language_it_it, "it-IT"), + + /** + * Japanese (Japan) + */ + JA_JP(R.string.fcr_dialog_rtt_language_ja_jp, "ja-JP"), + + /** + * Korean (South Korea) + */ + KO_KR(R.string.fcr_dialog_rtt_language_ko_kr, "ko-KR"), + + /** + * Malay (Malaysia) + */ + MS_MY(R.string.fcr_dialog_rtt_language_ms_my, "ms-MY*"), + + /** + * Persian (Iran) + */ + FA_IR(R.string.fcr_dialog_rtt_language_fa_ir, "fa-IR*"), + + /** + * Portuguese (Portugal) + */ + PT_PT(R.string.fcr_dialog_rtt_language_pt_pt, "pt-PT"), + + /** + * Russian (Russia) + */ + RU_RU(R.string.fcr_dialog_rtt_language_ru_ru, "ru-RU"), + + /** + * Spanish (Spain) + */ + ES_ES(R.string.fcr_dialog_rtt_language_es_es, "es-ES"), + + /** + * Turkish (Turkey) + */ + TR_TR(R.string.fcr_dialog_rtt_language_tr_tr, "tr-TR"), + + /** + * Vietnamese (Vietnam) + */ + VI_VN(R.string.fcr_dialog_rtt_language_vi_vn, "vi-VN"), +} + +private interface IRttOptionsService { + /** + * 修改RTT配置信息 + */ + @Headers("Content-Type: application/json") + @PUT("edu/apps/{appId}/v2/rooms/{roomId}/widgets/rtt/states/{state}") + fun buildTokens( + @Path("appId") appId: String?, @Path("roomId") roomId: String?, @Path("state") state: Int?, + @Body body: FcrRttChangeOptionsData, + ): Call> +} + +/** + * 修改RTT配置操作请求实体 + */ +private data class FcrRttChangeOptionsData( + /** + * 语言配置 + */ + var languages: RttChangOptionsLanguage? = null, + + /** + * 是否开启转写 + */ + var transcribe: Int = 0, + + /** + * 是否开启字幕 + */ + var subtitle: Int = 0, +) { + /** + * 已用的体验时间,接收使用,请求的时候不需要 + */ + var duration: Int = 0 + + companion object { + fun formatUseData(openRttConversion: Boolean, openSubtitle: Boolean, sourceLan: String, targetLan: Array): FcrRttChangeOptionsData { + return FcrRttChangeOptionsData(RttChangOptionsLanguage(sourceLan, targetLan), if (openRttConversion) 1 else 0, if (openSubtitle) 1 else 0) + } + } +} + +/** + * Rtt-字幕逻辑管理 + */ +private class RttSubtitlesManager(private val rttOptionsManager: RttOptionsManager, private val listener: FcrRttOptionsStatusListener) { + + /** + * 是否开启成功 + */ + private var openSuccess = false + + /** + * 字幕开启成功 + */ + private val messageWhatOpenSuccess = 0 + + /** + * 无人讲话的消息类型 + */ + private val messageWhatNoSpeaking = 1 + + /** + * 正在聆听的消息类型 + */ + private val messageWhatListening = 2 + + /** + * 隐藏rtt的消息类型 + */ + private val messageWhatHidde = 3 + + /** + * 所有的定时相关的处理 + */ + private val handler: Handler = object : Handler(rttOptionsManager.rttOptions.getActivityContext().mainLooper) { + override fun handleMessage(msg: Message) { + super.handleMessage(msg) + when (msg.what) { + messageWhatOpenSuccess -> { + openSuccess = true + listener.subtitlesStateChange(true) + listener.audioStateSpeaking() + //显示两秒正在聆听 + sendEmptyMessageDelayed(messageWhatListening, 2000) + } + + messageWhatListening -> { + listener.audioStateNoSpeaking() + //显示两秒正在聆听 + sendEmptyMessageDelayed(messageWhatNoSpeaking, 2000) + } + + messageWhatNoSpeaking -> { + listener.audioStateNoSpeakingMoreTime() + } + + messageWhatHidde -> { + listener.subtitlesViewReset(false) + listener.subtitlesStateChange(false) + } + } + } + } + + /** + * 开启字幕 + * @param configAllowUse 配置是否允许rtt + * @param experienceDefaultTime 体验默认时间 + * @param experienceReduceTime 体验剩余时间 + */ + fun openSubtitles(configAllowUse: Boolean, experienceDefaultTime: Int, experienceReduceTime: Int) { + //清除消息 + this.clearHandlerMsg() + listener.subtitlesViewReset(true) + if (rttOptionsManager.isAllowUseRtt()) { + //可以使用的话先隐藏体验,后续再根据条件判断是否显示 + listener.experienceInfoChange(configAllowUse, experienceDefaultTime, experienceReduceTime) + //显示文案 + listener.audioStateOpening() + if (openSuccess) { + listener.audioStateNoSpeakingMoreTime() + } else { + //发起开启rtt请求 + rttOptionsManager.sendRequest(rttOptionsManager.isOpenConversion(), openRttSubtitles = true, + callback = object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + //延迟两秒开启文案:点击字幕位置可以更改字幕设置 + listener.audioStateShowSettingHint() + rttOptionsManager.startExperience() + handler.sendEmptyMessageDelayed(messageWhatOpenSuccess, 2000) + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + openSuccess = false + listener.subtitlesStateChange(false) + } + }) + } + } else { + listener.experienceInfoChange(configAllowUse, experienceDefaultTime, experienceReduceTime) + listener.audioStateNotAllowUse() + openSuccess = false + handler.sendEmptyMessageDelayed(messageWhatHidde, 2000) + } + } + + /** + * 关闭字幕 + */ + fun closeSubtitles() { + //清除消息 + this.clearHandlerMsg() + //发起开启rtt请求 + if (openSuccess) { + rttOptionsManager.sendRequest(rttOptionsManager.isOpenConversion(), false, object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + openSuccess = false + listener.subtitlesStateChange(false) + if (!rttOptionsManager.isOpenConversion()) { + rttOptionsManager.stopExperience() + } + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + } + }) + } + openSuccess = false + listener.subtitlesViewReset(false) + } + + /** + * 是否开启了字幕 + */ + fun isOpenSubtitles(): Boolean { + return openSuccess + } + + /** + * 初始化字幕状态 + */ + fun initOpenSubtitle(state: Boolean) { + openSuccess = state + if (openSuccess) { + listener.subtitlesViewReset(true) + rttOptionsManager.startExperience() + //显示文案 + listener.audioStateOpening() + handler.sendEmptyMessage(messageWhatOpenSuccess) + } + } + + /** + * 设置当前翻译数据 + */ + fun setShowCurrentData(recordItem: RttRecordItem?, currentSettingInfo: RttSettingInfo) { + if (isOpenSubtitles()) { +// listener.subtitlesViewReset(true) + //清除消息 + this.clearHandlerMsg() + + val showText = recordItem?.getShowText() + val translating = 2 == (showText?.size ?: 0) && showText?.get(0).isNullOrEmpty() && showText?.get(1).isNullOrEmpty() + if (2 != (showText?.size ?: 0) || showText?.get(0).isNullOrEmpty() && !translating) { + listener.audioStateNoSpeaking() + } else if (translating) { + listener.audioStateSpeaking() + } else { + listener.onMessageChange(recordItem) +// rttBottomCenterSubtitlesView?.setShowTranslatorsInfo(recordItem?.userHeader ?: "", recordItem?.userName ?: "", sourceText ?: "", +// targetText) + } + //房间内有音频输出时,字幕区域显示对应转写文字,无音频输出时,字幕区域在音频停止3S后自动消失 + handler.sendEmptyMessageDelayed(messageWhatNoSpeaking, 3000) + } + } + + /** + * 清除定时handler消息 + */ + private fun clearHandlerMsg() { + handler.removeMessages(messageWhatOpenSuccess) + handler.removeMessages(messageWhatListening) + handler.removeMessages(messageWhatNoSpeaking) + handler.removeMessages(messageWhatHidde) + } +} + +/** + * Rtt-转写逻辑管理 + */ +private class RttConversionManager(private val rttOptionsManager: RttOptionsManager, private val listener: FcrRttOptionsStatusListener) { + + /** + * 是否开启成功 + */ + private var openSuccess = false + + /** + * Rtt转写弹窗 + */ + private val rttConversionDialog by lazy { + AgoraUIRttConversionDialogBuilder(rttOptionsManager.rttOptions.getActivityContext()).build().apply { +// setOnDismissListener { +// this.optionsCallback!!.closeConversion() +// } + optionsCallback = object : ConversionOptionsInterface { + /** + * 开启转写 + */ + override fun openConversion() { + if (rttOptionsManager.isOpenConversion()) { + openSuccess = true + listener.conversionStateChange(true) + rttOptionsManager.startExperience() + } else { + if (!openSuccess) { + rttOptionsManager.sendRequest(true, rttOptionsManager.isOpenSubtitles(), + callback = object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + openSuccess = true + listener.conversionStateChange(true) + rttOptionsManager.startExperience() + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + openSuccess = false + listener.conversionStateChange(false) + } + }) + } else { + listener.conversionStateChange(true) + } + } + + } + + /** + * 关闭转写 + */ + override fun closeConversion() { + if (rttOptionsManager.isOpenConversion()) { + if (openSuccess) { + rttOptionsManager.sendRequest(false, rttOptionsManager.isOpenSubtitles(), + callback = object : HttpCallback>() { + override fun onSuccess(result: HttpBaseRes?) { + openSuccess = false + listener.conversionStateChange(false) + rttOptionsManager.stopExperience() + } + + override fun onError(httpCode: Int, code: Int, message: String?) { + } + }) + } else { + listener.conversionStateChange(false) + } + } else { + openSuccess = false + listener.conversionStateChange(false) + } + } + + /** + * 打开设置页面 + */ + override fun openSetting() { + rttOptionsManager.openSetting() + } + + } + } + } + + /** + * 开启转写 + */ + fun openConversion(list: List) { + listener.conversionViewReset() + rttConversionDialog.show(list) + if (rttOptionsManager.isAllowUseRtt()) { + rttConversionDialog.optionsCallback!!.openConversion() + } + } + + /** + * 关闭转写 + */ + fun closeConversion() { + listener.conversionViewReset() + if (rttOptionsManager.isAllowUseRtt()) { + rttConversionDialog.dismiss() + rttConversionDialog.optionsCallback!!.closeConversion() + } + } + + /** + * 是否开启了转写 + */ + fun isOpenConversion(): Boolean { + return openSuccess + } + + /** + * 初始化转写状态 + */ + fun initOpenConversion(state: Boolean) { + openSuccess = state + rttOptionsManager.rttOptions.runOnUiThread { + listener.conversionViewReset() + listener.conversionStateChange(openSuccess) + rttConversionDialog.changeShowButton(openSuccess) + if (openSuccess) { + rttOptionsManager.startExperience() + } else { + rttOptionsManager.stopExperience() + } + } + } + + /** + * 设置体验信息 + */ + fun setExperienceInfo(allowUseConfig: Boolean, rttExperienceReduceTime: Int) { + rttConversionDialog.setExperienceInfo(allowUseConfig, rttExperienceReduceTime) + } + + /** + * 新增转写数据 + */ + fun updateShowList(list: List) { + try { + rttConversionDialog.updateShowList(list) + } catch (ignore: Exception) { + } + } + + /** + * 新增状态改变文本消息 + */ + fun addStateChangeTextMessage(transcribe: Int, userInfo: EduBaseUserInfo, localUserInfo: AgoraEduContextUserInfo?, useManager: RttUseManager) { + val toOpen = 1 == transcribe + val appContext = rttOptionsManager.rttOptions.getApplicationContext() + val textContent = "${rttOptionsManager.formatRoleName(userInfo, localUserInfo)}${ + rttOptionsManager.rttOptions.getApplicationContext() + .getString(if (toOpen) R.string.fcr_dialog_rtt_text_conversion_state_open else R.string.fcr_dialog_rtt_text_conversion_state_close) + }" + val toastContent = "${rttOptionsManager.formatRoleName(userInfo, localUserInfo)}${ + rttOptionsManager.rttOptions.getApplicationContext().getString( + if (userInfo.userUuid == localUserInfo?.userUuid) { + if (toOpen) R.string.fcr_dialog_rtt_toast_conversion_state_open_me_show else R.string.fcr_dialog_rtt_toast_conversion_state_close_me_show + } else { + if (toOpen) R.string.fcr_dialog_rtt_toast_conversion_state_open else R.string.fcr_dialog_rtt_toast_conversion_state_close + } + ) + }" + if (!rttOptionsManager.isOpenConversion() && toOpen && userInfo.userUuid != localUserInfo?.userUuid) { + rttOptionsManager.rttOptions.runOnUiThread { + AgoraUIDialogBuilder(rttOptionsManager.rttOptions.getActivityContext()).title(appContext.resources.getString(R.string.fcr_dialog_rtt_conversion)) + .message(textContent).messagePaddingHorizontal(appContext.resources.getDimensionPixelOffset(R.dimen.dp_10)) + .negativeText(appContext.resources.getString(R.string.fcr_dialog_rtt_oher_change_source_hint_cancel)) + .positiveText(appContext.resources.getString(R.string.fcr_dialog_rtt_oher_change_source_hint_confirm)).positiveClick { + openConversion(useManager.getShowRecordList()) + }.build().show() + } + } + AgoraUIToast.showDefaultToast(appContext, toastContent) + useManager.disposeDataChangeSourceLanguage(userInfo, textContent) + rttOptionsManager.rttOptions.runOnUiThread { updateShowList(useManager.getShowRecordList()) } + LogX.i(TAG, "changeConversionState result=$textContent}") + } + + /** + * 新增目标文本改变消息 + */ + fun addTargetLanguageChangeMessage(open: Boolean, userInfo: EduBaseUserInfo, useManager: RttUseManager) { + val showContent = rttOptionsManager.rttOptions.getApplicationContext().getString(R.string.fcr_dialog_rtt_text_open_target_language) + useManager.disposeDataChangeSourceLanguage(userInfo, showContent) + rttOptionsManager.rttOptions.runOnUiThread { updateShowList(useManager.getShowRecordList()) } + LogX.i(TAG, "changeToOpenTargetLanguage result=$showContent}") + } + + /** + * 新增声源语言改变消息 + */ + fun addSourceLanguageChangeMessage( + userInfo: EduBaseUserInfo, localUserInfo: AgoraEduContextUserInfo?, useManager: RttUseManager, + settingsManager: RttSettingManager, sourceEnum: RttLanguageEnum, + ) { + //前置名称 + val useText = "${ + rttOptionsManager.formatRoleName(userInfo, localUserInfo) + }${rttOptionsManager.rttOptions.getApplicationContext().getString(R.string.fcr_dialog_rtt_text_change_source_language)}" + val languageText = rttOptionsManager.rttOptions.getApplicationContext().getString(sourceEnum.textRes) + val showContent = SpannableString("$useText${languageText}") + showContent.setSpan( + ForegroundColorSpan(ContextCompat.getColor(rttOptionsManager.rttOptions.getApplicationContext(), R.color.fcr_blue_4262)), + showContent.lastIndexOf(languageText), showContent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + AgoraUIToast.showDefaultToast(rttOptionsManager.rttOptions.getApplicationContext(), showContent) + //通知处理 + useManager.disposeDataChangeSourceLanguage(userInfo, showContent.toString()) + updateShowList(useManager.getShowRecordList()) + LogX.i(TAG, "changeSourceLanguage source=${settingsManager.currentSettingInfo.getSourceLan()}, result=$sourceEnum}") + + } +} + +/** + * Rtt-设置弹窗管理 + */ +private class RttSettingManager(private val rttOptionsManager: RttOptionsManager) { + /** + * RTT/转写设置弹窗 + */ + private val rttSettingDialog by lazy { + AgoraUIRttSettingBuilder(rttOptionsManager.rttOptions.getActivityContext()).setListener(object : AgoraUIRttSettingDialogListener { + /** + * 修改双语显示 + */ + override fun changeDoubleShow(showDouble: Boolean) { + currentSettingInfo.setShowDoubleLan(showDouble) + rttOptionsManager.changeDoubleShow(showDouble) + } + + /** + * 设置目标语言 + */ + override fun setTargetLan(code: String) { + RttLanguageEnum.values().find { it.value == code }?.let { + rttOptionsManager.setTargetLanguage(it) + } + } + + /** + * 设置声源语言 + */ + override fun setSourceLan(code: String) { + RttLanguageEnum.values().find { it.value == code }?.let { + rttOptionsManager.setSourceLanguage(it) + } + } + }).build() + } + + /** + * 当前语言的管理信息 + */ + val currentSettingInfo by lazy { RttSettingInfo(rttOptionsManager) } + + /** + * 开启设置 + */ + fun openSetting() { + if (rttOptionsManager.isAllowUseRtt()) { + rttSettingDialog.show(currentSettingInfo) + } + } + + /** + * 关闭设置 + */ + fun closeSetting() { + rttSettingDialog.dismiss() + } +} + +/** + * Rtt-使用管理 + * @param configAllowUse RTT功能是否允许使用,默认不可以使用,不可使用的话则提供体验时间 + */ +private class RttUseManager( + private val rttOptionsManager: RttOptionsManager, var configAllowUse: Boolean = false, + private val listener: FcrRttOptionsStatusListener, +) { + /** + * rtt功能默认体验时间,默认十分钟 + */ + val rttExperienceDefaultTime: Int = 600000 + + /** + * rtt功能剩余体验时间,默认十分钟 + */ + var rttExperienceReduceTime: Int = rttExperienceDefaultTime + private set + + /** + * 所有的定时相关的处理 + */ + private val handler: Handler = object : Handler(rttOptionsManager.rttOptions.getActivityContext().mainLooper) { + override fun handleMessage(msg: Message) { + super.handleMessage(msg) + rttExperienceReduceTime -= 1000 + listener.experienceInfoChange(configAllowUse, rttExperienceDefaultTime, rttExperienceReduceTime) + if (rttExperienceReduceTime <= 0) { + listener.audioStateNotAllowUse() + stopExperience() + } else { + sendEmptyMessageDelayed(msg.what, 1000) + } + } + } + + /** + * 总记录数据列表 + */ + private val allRecordList = arrayListOf() + + /** + * 转写显示的记录列表 + */ + private val showRecordList = arrayListOf() + + /** + * 最后一条信息 + */ + private var lastRecord: RttRecordItem? = null + + /** + * 是否允许体验 + */ + fun isAllowUse(): Boolean { + return this.configAllowUse || this.rttExperienceReduceTime > 0 + } + + /** + * 设置体验剩余时间 + */ + fun setExperienceReduceTime(time: Int) { + this.rttExperienceReduceTime = time.coerceAtMost(this.rttExperienceReduceTime).coerceAtLeast(0) + } + + /** + * 开始体验 + */ + fun startExperience() { + if (isAllowUse() && rttExperienceReduceTime > 0) { + listener.experienceInfoChange(configAllowUse, rttExperienceDefaultTime, rttExperienceReduceTime) + handler.removeMessages(0) + handler.sendEmptyMessageDelayed(0, 1000) + } + } + + /** + * 结束体验 + */ + fun stopExperience() { + handler.removeMessages(0) + } + + /** + * 声源语言调整数据处理 + */ + fun disposeDataChangeSourceLanguage(userInfo: EduBaseUserInfo, text: String): RttRecordItem { + return RttRecordItem().apply { + uuid = UUID.randomUUID().toString() + userName = userInfo.userName + statusText = text + if (allRecordList.isEmpty() || allRecordList.isNotEmpty() && text != allRecordList[allRecordList.size - 1].statusText) { + allRecordList.add(this) + showRecordList.add(this) + } + } + } + + /** + * 处理数据 + */ + fun disposeData(rttMsgData: AgoraSpeech2TextProtobuffer.Text, settingInfo: RttSettingInfo, streamContext: StreamContext?): RttRecordItem? { + val lastItem = (if (allRecordList.isEmpty()) null else allRecordList[allRecordList.size - 1]) + val lastItemByUid = lastItem?.uid + var paramsData: RttRecordItem? = null + when (rttMsgData.dataType) { + //转写 + "transcribe" -> { + val sourceTextStr = StringBuffer() + var final = false + var confidence = 0.0 + rttMsgData.wordsList.forEach { word -> + sourceTextStr.append(word.text) + final = word.isFinal + confidence = word.confidence + } + LogX.i(TAG, "transcribe: $lastItemByUid$$$$$$$$sourceTextStr") + if (lastItemByUid == null || rttMsgData.uid != lastItemByUid || lastItem.isFinal) { + paramsData = RttRecordItem().apply { + uuid = UUID.randomUUID().toString() + currentTargetLan = settingInfo.getTargetLan() + showDoubleLan = settingInfo.isShowDoubleLan() + sourceLan = RttLanguageEnum.values().find { it.value == rttMsgData.culture } + sourceText = sourceTextStr.toString() + uid = rttMsgData.uid + time = if (final) if (rttMsgData.time == 0L) rttMsgData.time else System.currentTimeMillis() else rttMsgData.time + isFinal = final + sourceConfidence = confidence + } + allRecordList.add(paramsData) + } else { + paramsData = allRecordList.findLast { it.uid == rttMsgData.uid }?.apply { + uuid = UUID.randomUUID().toString() + sourceText = sourceTextStr.toString() + time = if (final) if (rttMsgData.time == 0L) rttMsgData.time else System.currentTimeMillis() else rttMsgData.time + isFinal = final + sourceConfidence = confidence + } + } + sourceTextStr.setLength(0) + } + //翻译 + "translate" -> { + LogX.i(TAG, "Translation:" + GsonUtil.toJson(rttMsgData)) + val last = allRecordList.findLast { it.uid == lastItemByUid } + val tranList = last?.targetInfo ?: arrayListOf() + val transTextStr = StringBuffer() + rttMsgData.transList.forEach { transItem -> + transItem.textsList.forEach { text -> + LogX.i(TAG, "Translation:$lastItemByUid$$$$$$$$$text") + transTextStr.append(text) + } + tranList.find { transItem.lang == it.language?.value }.let { find -> + if (find != null) { + find.text = transTextStr.toString() + } else { + tranList.add(RttRecordItemTran().apply { + language = RttLanguageEnum.values().find { it.value == transItem.lang } + text = transTextStr.toString() + }) + } + } + transTextStr.setLength(0) + } + + paramsData = allRecordList.findLast { it.uid == lastItemByUid }?.apply { + uuid = UUID.randomUUID().toString() + showDoubleLan = settingInfo.isShowDoubleLan() + targetInfo = tranList + targetText = tranList.find { item -> (item.language?.value ?: "") == currentTargetLan?.value }?.text + } + } + } + lastRecord = paramsData + if (lastRecord != null && rttOptionsManager.isOpenConversion()) { + val showItem = if (showRecordList.isNotEmpty()) showRecordList[showRecordList.size - 1] else null + if (showItem?.uuid != null && showItem.uuid == lastRecord!!.uuid) { + showRecordList[showRecordList.size - 1] = lastRecord!! + } else { + showRecordList.add(lastRecord!!) + } + } + //格式化所有的用户信息 + formatAllUserInfo(streamContext) + return paramsData + } + + /** + * 格式化所有的用户信息 + */ + fun formatAllUserInfo(streamContext: StreamContext?) { + allRecordList.forEach { item -> + streamContext?.getAllStreamList()?.find { it.streamUuid == item.uid?.toString() }?.owner?.let { info -> + item.userName = info.userName + item.userHeader = info.userName + } + } + showRecordList.forEach { item -> + streamContext?.getAllStreamList()?.find { it.streamUuid == item.uid?.toString() }?.owner?.let { info -> + item.userName = info.userName + item.userHeader = info.userName + } + } + lastRecord?.let { item -> + streamContext?.getAllStreamList()?.find { it.streamUuid == item.uid?.toString() }?.owner?.let { info -> + item.userName = info.userName + item.userHeader = info.userName + } + } + } + + /** + * 获取记录列表 + */ + fun getShowRecordList(): List { + return showRecordList.toList() + } + + /** + * 设置最后一条信息为最后 + */ + fun setLastFinal() { + if (allRecordList.isNotEmpty()) { + allRecordList[allRecordList.size - 1].isFinal = true + } + if (showRecordList.isNotEmpty()) { + showRecordList[showRecordList.size - 1].isFinal = true + } + lastRecord?.isFinal = true + } + + /** + * 体验时间是否用完 + */ + fun experienceReduceTimeZero(): Boolean { + return this.rttExperienceReduceTime <= 0 + } + +} + +/** + * Rtt操作状态监听 + */ +abstract class FcrRttOptionsStatusListener { + /** + * rtt功能状态变更 + * @param open 开启-true,关闭-false + */ + open fun rttStateChange(open: Boolean) {} + + /** + * 音频状态-无人讲话 + */ + open fun audioStateNoSpeaking() {} + + /** + * 音频状态-有人讲话 + */ + open fun audioStateSpeaking() {} + + /** + * 音频状态-超过一定时间无人讲话 + */ + open fun audioStateNoSpeakingMoreTime() {} + + /** + * 音频状态-开启中 + */ + open fun audioStateOpening() {} + + /** + * 音频状态-无法使用 + */ + open fun audioStateNotAllowUse() {} + + /** + * 音频状态-显示设置提示 + */ + open fun audioStateShowSettingHint() {} + + /** + * 体验信息变更 + * @param configAllowUseRtt 配置是否可以使用rtt功能 + * @param experienceReduceTime 剩余体验时间 + * @param experienceDefaultTime 默认体验时间 + */ + open fun experienceInfoChange(configAllowUseRtt: Boolean, experienceDefaultTime: Int, experienceReduceTime: Int) {} + + /** + * 字幕状态变更 + * @param toOpen 开启-true,关闭-false + */ + open fun subtitlesStateChange(toOpen: Boolean) {} + + /** + * 字幕视图重置 + */ + open fun subtitlesViewReset(openSuccess: Boolean) {} + + /** + * 转写状态变更 + * @param toOpen 开启-true,关闭-false + */ + open fun conversionStateChange(toOpen: Boolean) {} + + /** + * 转写视图重置 + */ + open fun conversionViewReset() {} + + /** + * 声源语言修改 + */ + open fun sourceLanguageChange(language: RttLanguageEnum) {} + + /** + * 目标语言修改-网络请求结果 + */ + open fun targetLanguageChange(languages: List) {} + + /** + * 双语状态变更 + * @param open 开启-true,关闭-false + */ + open fun showDoubleLanguage(open: Boolean) {} + + /** + * 双语状态变更-网络请求结果 + * @param open 开启-true,关闭-false + */ + open fun showDoubleLanguageNetResult(open: Boolean) {} + + /** + * 消息改变 + * @param currentData 当前要显示的数据 + */ + open fun onMessageChange(currentData: RttRecordItem?) {} +} diff --git a/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduOptionsComponent.kt b/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduOptionsComponent.kt index f28dacde3..4f9db43e5 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduOptionsComponent.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduOptionsComponent.kt @@ -8,12 +8,6 @@ import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.ImageView import androidx.core.content.ContextCompat -import io.agora.online.component.AgoraEduChatComponent -import io.agora.online.component.AgoraEduSettingComponent -import io.agora.online.component.common.AbsAgoraEduConfigComponent -import io.agora.online.component.common.IAgoraUIProvider -import io.agora.online.component.common.UIUtils -import io.agora.online.component.whiteboard.data.AgoraEduApplianceData import com.google.gson.Gson import io.agora.agoraeducore.core.AgoraEduCoreManager import io.agora.agoraeducore.core.context.AgoraEduContextUserInfo @@ -38,9 +32,17 @@ import io.agora.agoraeducore.core.internal.transport.OnAgoraTransportListener import io.agora.agoraeducore.extensions.widgets.bean.AgoraWidgetDefaultId import io.agora.agoraeducore.extensions.widgets.bean.AgoraWidgetMessageObserver import io.agora.online.R +import io.agora.online.component.AgoraEduChatComponent +import io.agora.online.component.AgoraEduSettingComponent +import io.agora.online.component.FcrRttToolBoxComponent +import io.agora.online.component.common.AbsAgoraEduConfigComponent +import io.agora.online.component.common.IAgoraUIProvider +import io.agora.online.component.common.UIUtils import io.agora.online.component.toast.AgoraUIToast +import io.agora.online.component.whiteboard.data.AgoraEduApplianceData import io.agora.online.config.FcrUIConfig import io.agora.online.databinding.FcrOnlineEduOptionsComponentBinding +import io.agora.online.helper.RttOptionsManager import io.agora.online.impl.chat.ChatPopupWidgetListener import io.agora.online.impl.whiteboard.bean.AgoraBoardGrantData import io.agora.online.impl.whiteboard.bean.AgoraBoardInteractionPacket @@ -59,12 +61,12 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite lateinit var rootContainer: ViewGroup // 给IM用的,view root lateinit var itemContainer: ViewGroup // 显示侧边栏 - private var binding: FcrOnlineEduOptionsComponentBinding = - FcrOnlineEduOptionsComponentBinding.inflate(LayoutInflater.from(context), this, true) + private var binding: FcrOnlineEduOptionsComponentBinding = FcrOnlineEduOptionsComponentBinding.inflate(LayoutInflater.from(context), this, true) private var agroSettingWidget: AgoraEduSettingComponent? = null private var popupViewRoster: AgoraEduRosterComponent? = null private var popupViewChat: AgoraEduChatComponent? = null + private var rttToolBoxWidget: FcrRttToolBoxComponent? = null private lateinit var optionPresenter: AgoraEduOptionPresenter var onExitListener: (() -> Unit)? = null // 退出 private var isRequestHelp = false // 分组是否请求了帮助 @@ -75,10 +77,15 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite AgoraTransportManager.addListener(AgoraTransportEventId.EVENT_ID_OPTIONS_PANEL, this) } - fun initView(uuid: String, rootContainer: ViewGroup, itemContainer: ViewGroup, agoraUIProvider: IAgoraUIProvider) { + fun initView( + uuid: String, rootContainer: ViewGroup, itemContainer: ViewGroup, + agoraUIProvider: IAgoraUIProvider, + ) { this.uuid = uuid this.itemContainer = itemContainer this.rootContainer = rootContainer +// this.rttOptionsManager = rttOptionsManager +// this.rttOptionsManager.setEduOptionsComponent(this) initView(agoraUIProvider) } @@ -142,12 +149,23 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite } binding.optionItemSetting.setOnClickListener { //LogX.e(TAG,">>>${eduContext?.userContext()?.getCoHostList()}") - if (!binding.optionItemSetting.isActivated) { + if (!it.isActivated) { showItem(agroSettingWidget) setIconActivated(binding.optionItemSetting) } else { hiddenItem() - binding.optionItemSetting.isActivated = false + it.isActivated = false + } + } + binding.optionItemRtt.setOnClickListener { + //LogX.e(TAG,">>>${eduContext?.userContext()?.getCoHostList()}") + if (!it.isActivated) { + rttToolBoxWidget!!.resetEduRttToolBoxStatus() + showItem(rttToolBoxWidget, R.dimen.agora_edu_options_rtt_dialog_w, R.dimen.agora_userlist_dialog_large_h) + setIconActivated(binding.optionItemRtt) + } else { + hiddenItem() + it.isActivated = false } } binding.optionItemRoster.setOnClickListener { @@ -344,6 +362,12 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite popupViewRoster = AgoraEduRosterComponent(context) popupViewRoster?.initView(agoraUIProvider) } + //Rtt功能只在非分组状态下使用 + if(eduContext?.roomContext()?.getRoomInfo()?.roomType?.value == RoomType.GROUPING_CLASS.value){ + binding.optionItemRtt.visibility = VISIBLE + }else{ + binding.optionItemRtt.visibility = GONE + } } /** @@ -415,11 +439,34 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemChatNews.visibility = View.GONE } + /** + * 初始化Rtt + */ + fun initRtt(conversionStatusView: ViewGroup, subtitleView: AgoraEduRttOptionsComponent) { + //初始化聊天组件 + if (rttToolBoxWidget == null) { + rttToolBoxWidget = FcrRttToolBoxComponent(context) + rttToolBoxWidget?.initView(agoraUIProvider,this, conversionStatusView, subtitleView) + } + } + + /** + * 隐藏rtt相关的 + */ + fun hiddenRtt() { + post { + hiddenItem() + binding.optionItemRtt.isActivated = false + } + } + fun setHandsupTimeout(seconds: Int) { //binding.optionItemHandup.setHandsupTimeout(seconds) } private fun showItem(item: View?) { + //如果体验结束的话,那么则隐藏弹窗view,做收口处理 + rttToolBoxWidget?.resetShowDialogIfEnd() itemContainer.removeAllViews() itemContainer.addView(item) } @@ -435,14 +482,24 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemSetting -> { binding.optionItemRoster.isActivated = false binding.optionItemChat.isActivated = false + binding.optionItemRtt.isActivated = false } + + binding.optionItemRtt -> { + binding.optionItemRoster.isActivated = false + binding.optionItemChat.isActivated = false + } + binding.optionItemRoster -> { binding.optionItemSetting.isActivated = false binding.optionItemChat.isActivated = false + binding.optionItemRtt.isActivated = false } + binding.optionItemChat -> { binding.optionItemRoster.isActivated = false binding.optionItemSetting.isActivated = false + binding.optionItemRtt.isActivated = false } } } @@ -492,6 +549,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite when (roomType) { RoomType.ONE_ON_ONE -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemChat.visibility = View.VISIBLE binding.optionItemRoster.visibility = View.GONE @@ -499,8 +557,10 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemWhiteboardTool.visibility = GONE initChat() } + RoomType.SMALL_CLASS -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.VISIBLE binding.optionItemToolbox.visibility = View.GONE binding.optionItemRoster.visibility = View.VISIBLE binding.optionItemHandup.visibility = View.VISIBLE @@ -509,17 +569,21 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemChat.visibility = View.VISIBLE initChat() } + RoomType.LARGE_CLASS -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemChat.visibility = View.GONE binding.optionItemRoster.visibility = View.GONE binding.optionItemHandup.visibility = View.VISIBLE binding.optionItemWhiteboardTool.visibility = GONE } + RoomType.GROUPING_CLASS -> { binding.optionItemAsking.visibility = View.VISIBLE binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemRoster.visibility = View.VISIBLE binding.optionItemHandup.visibility = View.VISIBLE @@ -538,6 +602,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemRoster.visibility = View.GONE binding.optionItemHandup.visibility = View.GONE } + RoomType.SMALL_CLASS -> { binding.optionItemRoster.visibility = View.VISIBLE binding.optionItemHandup.visibility = View.VISIBLE @@ -545,6 +610,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemChat.visibility = View.VISIBLE initChat() } + RoomType.LARGE_CLASS -> { binding.optionItemChat.visibility = View.GONE binding.optionItemRoster.visibility = View.VISIBLE @@ -555,6 +621,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite } binding.optionItemToolbox.visibility = View.GONE + binding.optionItemRtt.visibility = View.GONE binding.optionItemSetting.visibility = View.VISIBLE //setWhiteboardViewTool(AgoraEduApplianceData.isGrantBoard(eduCore)) } else { @@ -562,6 +629,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite when (roomType) { RoomType.ONE_ON_ONE -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemChat.visibility = View.VISIBLE initChat() @@ -569,8 +637,10 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite binding.optionItemHandup.visibility = View.GONE binding.optionItemWhiteboardTool.visibility = GONE } + RoomType.SMALL_CLASS -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemRoster.visibility = View.GONE binding.optionItemHandup.visibility = View.GONE @@ -582,6 +652,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite RoomType.LARGE_CLASS -> { binding.optionItemSetting.visibility = View.VISIBLE + binding.optionItemRtt.visibility = View.GONE binding.optionItemToolbox.visibility = View.GONE binding.optionItemChat.visibility = View.GONE binding.optionItemRoster.visibility = View.GONE @@ -633,6 +704,7 @@ class AgoraEduOptionsComponent : AbsAgoraEduConfigComponent, IWhite super.release() popupViewChat?.release() agroSettingWidget?.release() + rttToolBoxWidget?.release() popupViewRoster?.release() eduContext?.roomContext()?.removeHandler(roomHandler) eduContext?.userContext()?.removeHandler(userHandler) diff --git a/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduRttOptionsComponent.kt b/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduRttOptionsComponent.kt new file mode 100644 index 000000000..ba6b4152c --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/options/AgoraEduRttOptionsComponent.kt @@ -0,0 +1,199 @@ +package io.agora.online.options + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.constraintlayout.widget.ConstraintLayout +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import io.agora.online.R +import io.agora.online.component.common.AbsAgoraEduComponent +import io.agora.online.component.common.IAgoraUIProvider +import io.agora.online.databinding.FcrOnlineEduRttOptionsComponentBinding +import io.agora.online.helper.RttOptionsManager +import java.text.MessageFormat + + +class AgoraEduRttOptionsComponent : AbsAgoraEduComponent { + constructor(context: Context) : super(context) + constructor(context: Context, attr: AttributeSet) : super(context, attr) + constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) + + private var binding: FcrOnlineEduRttOptionsComponentBinding = + FcrOnlineEduRttOptionsComponentBinding.inflate(LayoutInflater.from(context), this, true) + + private var rttOptionsManager: RttOptionsManager? = null + + init { + binding.root.setOnClickListener { + rttOptionsManager?.openSetting() + } + } + + fun initView(rttOptionsManager: RttOptionsManager, agoraUIProvider: IAgoraUIProvider) { + super.initView(agoraUIProvider) + this.rttOptionsManager = rttOptionsManager + binding.agoraFcrRttTextDialogCloseView.setOnClickListener { + rttOptionsManager.closeSubtitles() + } + } + + private var touchX: Float = 0F + private var touchY: Float = 0F + private var move = false + override fun dispatchTouchEvent(event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> { + touchX = event.x + touchY = event.y + move = false + } + + MotionEvent.ACTION_MOVE -> { + if (Math.abs(event.x - touchX) > 30 || Math.abs(event.y - touchY) > 30) { + move = true + val params = layoutParams as MarginLayoutParams + params.leftMargin = Math.max(0, Math.min(params.leftMargin + (event.x - touchX).toInt(), (parent as View).width - width)) + params.bottomMargin = Math.max(0, Math.min(params.bottomMargin - (event.y - touchY).toInt(), (parent as View).height - height)) + setLayoutParams(params) + return true + } + } + + MotionEvent.ACTION_UP -> { + if (move) { + return true + } + } + } + return super.dispatchTouchEvent(event) + } + + /** + * 重置显示位置 + */ + fun resetShowPosition() { + runOnUIThread { + visibility = View.INVISIBLE + if (width == 0) { + viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + override fun onGlobalLayout() { + resetShowPosition() + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + } else { + val params = layoutParams as MarginLayoutParams + params.leftMargin = ((parent as View).width - width) / 2 + params.bottomMargin = 100 + if (params is ConstraintLayout.LayoutParams) { + params.topToTop = -1 + params.bottomToBottom = (parent as View).id + } + setLayoutParams(params) + visibility = View.VISIBLE + } + } + } + + /** + * 显示的时候需要再树布局测绘完成后再显示 + */ + override fun setVisibility(visibility: Int) { + runOnUIThread { + super.setVisibility(visibility) + } + } + + /** + * 设置限时体验信息 + */ + fun setExperienceInfo(allowUseConfig: Boolean, rttExperienceDefaultTime: Int, rttExperienceReduceTime: Int) { + runOnUIThread { + if (allowUseConfig) { + binding.agoraFcrRttTextDialogHintLayout.visibility = View.GONE + } else { + binding.agoraFcrRttTextDialogHintLayout.visibility = View.VISIBLE + if (rttExperienceReduceTime <= 0) { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setText(R.string.fcr_dialog_rtt_time_limit_end) + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.text = + MessageFormat.format(resources.getString(R.string.fcr_dialog_rtt_subtitles_dialog_time_limit_end), + rttExperienceDefaultTime / 60000) + } else { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.setText(R.string.fcr_dialog_rtt_time_limit) + binding.fcrOnlineEduRttConversionDialogTimeLimitReduce.text = MessageFormat.format( + resources.getString(R.string.fcr_dialog_rtt_subtitles_dialog_time_limit_reduce), + rttExperienceDefaultTime / 60000, + if (rttExperienceReduceTime / 60000 > 9) rttExperienceReduceTime / 60000 else "0${rttExperienceReduceTime / 60000}", + if (rttExperienceReduceTime % 60000 / 1000 > 9) rttExperienceReduceTime % 60000 / 1000 else "0${rttExperienceReduceTime % 60000 / 1000}", + ) + } + } + } + } + + /** + * 设置显示状态信息 + * @param showIcon 是否显示图标 + * @param showProgress 是否显示进度圈 + */ + fun setShowStatusInfo(showProgress: Boolean, showIcon: Boolean, text: String) { + runOnUIThread { + binding.agoraFcrRttTextDialogLayoutStatus.visibility = View.VISIBLE + binding.agoraFcrRttTextDialogLayoutText.visibility = View.GONE + binding.agoraFcrRttTextDialogStatusText.text = text + binding.agoraFcrRttTextDialogProgress.visibility = if (showProgress) VISIBLE else GONE + binding.agoraFcrRttTextDialogIcon.visibility = if (showIcon) VISIBLE else GONE + } + } + + /** + * 设置显示翻译信息 + * @param headImage 头像 + * @param name 用户名称 + * @param originText 翻译原文 + * @param resultText 翻译结果 + */ + fun setShowTranslatorsInfo(headImage: String, name: String, originText: String, resultText: String? = null) { + runOnUIThread { + binding.agoraFcrRttTextDialogLayoutStatus.visibility = View.GONE + binding.agoraFcrRttTextDialogLayoutText.visibility = View.VISIBLE + Glide.with(this).load(headImage).skipMemoryCache(true).placeholder(R.drawable.agora_video_ic_audio_on) + .apply(RequestOptions.circleCropTransform()).into(binding.agoraFcrRttTextDialogUserHeader) + binding.agoraFcrRttTextDialogUserHeaderText.text = if (headImage.isEmpty()) "" else headImage.substring(0, 1) + binding.agoraFcrRttTextDialogUserName.text = name + //语言显示 + val showDouble = rttOptionsManager?.isShowDoubleLan() ?: false + val leve2Text = if(showDouble) resultText else null + val leve1Text = if(!showDouble && !resultText.isNullOrEmpty()) resultText else originText + + binding.agoraFcrRttTextDialogTextOrigin.text = leve1Text + binding.agoraFcrRttTextDialogTextResult.text = resultText + binding.agoraFcrRttTextDialogTextResult.visibility = if (leve2Text.isNullOrEmpty()) View.GONE else View.VISIBLE + } + } + +} + + + + + + + + + + + + + + + + + + + + diff --git a/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassActivity.kt b/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassActivity.kt index 259fea505..e3ce6163d 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassActivity.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassActivity.kt @@ -4,32 +4,38 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.core.content.ContextCompat -import io.agora.online.component.common.UIUtils -import io.agora.online.helper.AgoraUIDeviceSetting -import io.agora.online.helper.FcrHandsUpManager -import io.agora.online.helper.RoomPropertiesHelper -import io.agora.agoraeducore.core.context.* +import io.agora.agoraeducore.core.context.AgoraEduContextClassState +import io.agora.agoraeducore.core.context.AgoraEduContextSystemDevice +import io.agora.agoraeducore.core.context.AgoraEduContextUserInfo +import io.agora.agoraeducore.core.context.EduContextCallback +import io.agora.agoraeducore.core.context.EduContextError +import io.agora.agoraeducore.core.context.EduContextRoomInfo +import io.agora.agoraeducore.core.context.FcrCustomMessage import io.agora.agoraeducore.core.internal.base.ToastManager import io.agora.agoraeducore.core.internal.framework.impl.handler.RoomHandler import io.agora.agoraeducore.core.internal.log.LogX import io.agora.agoraeducore.extensions.widgets.bean.AgoraWidgetDefaultId import io.agora.online.R +import io.agora.online.component.common.UIUtils import io.agora.online.component.dialog.AgoraUIDialog import io.agora.online.component.dialog.AgoraUIDialogBuilder import io.agora.online.component.teachaids.presenter.FCRSmallClassVideoPresenter import io.agora.online.component.toast.AgoraUIToast +import io.agora.online.databinding.ActivityAgoraOnlineClassBinding +import io.agora.online.helper.AgoraUIDeviceSetting +import io.agora.online.helper.FcrHandsUpManager +import io.agora.online.helper.RoomPropertiesHelper import io.agora.online.impl.whiteboard.bean.AgoraBoardInteractionPacket import io.agora.online.impl.whiteboard.bean.AgoraBoardInteractionSignal import io.agora.online.sdk.common.AgoraEduClassActivity import io.agora.online.sdk.presenter.AgoraClassVideoPresenter -import io.agora.online.databinding.ActivityAgoraOnlineClassBinding /** * author : hefeng * date : 2022/1/24 * description : 小班课(200) */ -open class AgoraOnlineClassActivity : AgoraEduClassActivity() { +open class AgoraOnlineClassActivity : AgoraEduClassActivity(){ override var TAG = "AgoraOnlineClassActivity" var agoraClassVideoPresenter: AgoraClassVideoPresenter? = null private lateinit var binding: ActivityAgoraOnlineClassBinding @@ -226,6 +232,7 @@ open class AgoraOnlineClassActivity : AgoraEduClassActivity() { // tool bar binding.agoraEduOptions.initView(uuid, binding.root, binding.agoraEduOptionsItemContainer, this) + binding.agoraEduOptions.initRtt(binding.agoraAreaBoardConversionStatus,binding.agoraRttOptions) launchConfig?.shareUrl?.let { binding.agoraEduOptions.setShareRoomLink(it) } @@ -246,6 +253,8 @@ open class AgoraOnlineClassActivity : AgoraEduClassActivity() { UIUtils.setViewVisible(binding.agoraClassUserListVideo, getUIConfig().isStageVisible) UIUtils.setViewVisible(binding.agoraEduWhiteboard, getUIConfig().isEngagementVisible) UIUtils.setViewVisible(binding.agoraEduOptions, getUIConfig().isEngagementVisible) + //初始化管理器 +// rttOptionsManager.initView(binding.agoraAreaBoardConversionStatus,binding.agoraRttOptions,this) } join() } @@ -303,4 +312,5 @@ open class AgoraOnlineClassActivity : AgoraEduClassActivity() { super.cancelHandsUp() binding.agoraEduOptions.cancelHandsUp() } + } \ No newline at end of file diff --git a/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassroomSDK.kt b/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassroomSDK.kt index 457ce867c..17e3baa73 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassroomSDK.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/sdk/AgoraOnlineClassroomSDK.kt @@ -1,17 +1,20 @@ package io.agora.online.sdk import android.content.Context -import io.agora.online.component.chat.AgoraChatRTMWidget -import io.agora.online.component.chat.AgoraEduEaseChatWidget import io.agora.agoraeducore.core.AgoraEduCore import io.agora.agoraeducore.core.ClassInfoCache import io.agora.agoraeducore.core.internal.framework.impl.managers.AgoraWidgetManager.Companion.registerDefault -import io.agora.agoraeducore.core.internal.framework.impl.managers.UserOnlineManager import io.agora.agoraeducore.core.internal.framework.proxy.RoomType -import io.agora.agoraeducore.core.internal.launch.* +import io.agora.agoraeducore.core.internal.launch.AgoraEduLaunchCallback +import io.agora.agoraeducore.core.internal.launch.AgoraEduLaunchConfig +import io.agora.agoraeducore.core.internal.launch.AgoraEduRegion +import io.agora.agoraeducore.core.internal.launch.AgoraEduSDK +import io.agora.agoraeducore.core.internal.launch.AgoraServiceType import io.agora.agoraeducore.core.internal.log.LogX import io.agora.agoraeducore.extensions.widgets.bean.AgoraWidgetConfig import io.agora.agoraeducore.extensions.widgets.bean.AgoraWidgetDefaultId +import io.agora.online.component.chat.AgoraChatRTMWidget +import io.agora.online.component.chat.AgoraEduEaseChatWidget import io.agora.online.component.teachaids.AgoraTeachAidCountDownWidget import io.agora.online.component.teachaids.AgoraTeachAidIClickerWidget import io.agora.online.component.teachaids.networkdisk.FCRCloudDiskWidget @@ -19,9 +22,11 @@ import io.agora.online.component.teachaids.vote.AgoraTeachAidVoteWidget import io.agora.online.component.teachaids.webviewwidget.FcrWebViewWidget import io.agora.online.impl.video.AgoraUILargeVideoWidget import io.agora.online.impl.whiteboard.AgoraWhiteBoardWidget -import io.agora.online.util.SpUtil import io.agora.online.sdk.common.AgoraBaseClassActivity import io.agora.online.sdk.helper.FCRLauncherManager +import io.agora.online.util.SpUtil +import io.agora.online.widget.FcrWidgetManager.WIDGETS_RTT_ID +import io.agora.online.widget.rtt.FcrRttToolBoxWidget /** * 一键拉起教室 @@ -157,6 +162,7 @@ object AgoraOnlineClassroomSDK { ) ) widgetConfigs.add(AgoraWidgetConfig(FCRCloudDiskWidget::class.java, AgoraWidgetDefaultId.AgoraCloudDisk.id)) + widgetConfigs.add(AgoraWidgetConfig(FcrRttToolBoxWidget::class.java, WIDGETS_RTT_ID, extraInfo = mutableMapOf())) return widgetConfigs } diff --git a/AgoraCloudScene/src/main/java/io/agora/online/widget/FcrWidgetManager.kt b/AgoraCloudScene/src/main/java/io/agora/online/widget/FcrWidgetManager.kt index d223a9108..7cf1c6aef 100644 --- a/AgoraCloudScene/src/main/java/io/agora/online/widget/FcrWidgetManager.kt +++ b/AgoraCloudScene/src/main/java/io/agora/online/widget/FcrWidgetManager.kt @@ -12,6 +12,7 @@ object FcrWidgetManager { val WIDGET_WEBVIEW_RUL = "webViewUrl" val WIDGET_WEBVIEW_TITLE = "webviewTitle" val WIDGET_WEBVIEW_MEDIAPLAYER = "mediaPlayer" + val WIDGETS_RTT_ID = "rtt" fun isWebViewWidget(widgetId: String): Boolean { return widgetId.contains(WIDGET_WEBVIEW) || widgetId.contains(WIDGET_WEBVIEW_MEDIAPLAYER) diff --git a/AgoraCloudScene/src/main/java/io/agora/online/widget/rtt/FcrRttToolBoxWidget.kt b/AgoraCloudScene/src/main/java/io/agora/online/widget/rtt/FcrRttToolBoxWidget.kt new file mode 100644 index 000000000..460b1f79b --- /dev/null +++ b/AgoraCloudScene/src/main/java/io/agora/online/widget/rtt/FcrRttToolBoxWidget.kt @@ -0,0 +1,308 @@ +package io.agora.online.widget.rtt + +import android.app.Activity +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.agora.agoraeducore.core.internal.framework.data.EduBaseUserInfo +import io.agora.agoraeducore.extensions.widgets.AgoraBaseWidget +import io.agora.online.R +import io.agora.online.component.common.IAgoraUIProvider +import io.agora.online.component.toast.AgoraUIToast +import io.agora.online.databinding.FcrOnlineToolBoxWidgetContentBinding +import io.agora.online.helper.FcrRttOptionsStatusListener +import io.agora.online.helper.IRttOptions +import io.agora.online.helper.RttOptionsManager +import io.agora.online.helper.RttRecordItem +import io.agora.online.options.AgoraEduOptionsComponent +import io.agora.online.options.AgoraEduRttOptionsComponent +import java.text.MessageFormat + +/** + * 功能作用:Rtt工具箱widget + * 初始注释时间: 2024/7/1 17:25 + * 创建人:王亮(Loren) + * 思路: + * 方法: + * 注意: + * 修改人: + * 修改时间: + * 备注: + * + * @author 王亮(Loren) + */ +class FcrRttToolBoxWidget : AgoraBaseWidget() { + override val TAG = "FcrRttToolsBoxWidget" + + private var contentView: AgoraRttToolBoxWidgetContent? = null + + + fun init( + container: ViewGroup, + agoraUIProvider: IAgoraUIProvider, agoraEduOptionsComponent: AgoraEduOptionsComponent, conversionStatusView: ViewGroup, + subtitleView: AgoraEduRttOptionsComponent, + ) { + super.init(container) + contentView = AgoraRttToolBoxWidgetContent(container, agoraUIProvider, agoraEduOptionsComponent, conversionStatusView, subtitleView) + } + + override fun release() { + contentView?.dispose() + super.release() + } + + override fun onWidgetRoomPropertiesUpdated( + properties: MutableMap, cause: MutableMap?, keys: MutableList, + operator: EduBaseUserInfo?, + ) { + super.onWidgetRoomPropertiesUpdated(properties, cause, keys, operator) + contentView?.onWidgetRoomPropertiesUpdated(properties, cause, keys, operator) + } + + /** + * 重置工具准给他 + */ + fun resetEduRttToolBoxStatus() { + contentView?.resetStatus() + } + + /** + * 如果体验结束的话,那么则隐藏弹窗view + */ + fun resetShowDialogIfEnd() { + contentView?.resetShowDialogIfEnd() + } + + + internal inner class AgoraRttToolBoxWidgetContent( + val container: ViewGroup, agoraUIProvider: IAgoraUIProvider, + agoraEduOptionsComponent: AgoraEduOptionsComponent?, conversionStatusView: ViewGroup?, val subtitleView: AgoraEduRttOptionsComponent?, + ) : IRttOptions { + private val listener = object : FcrRttOptionsStatusListener() { + override fun conversionViewReset() { + super.conversionViewReset() + agoraEduOptionsComponent?.hiddenRtt() + conversionStatusView?.visibility = View.GONE + resetStatus() + } + + override fun subtitlesViewReset(openSuccess: Boolean) { + super.subtitlesViewReset(openSuccess) + subtitleView?.resetShowPosition() + agoraEduOptionsComponent?.hiddenRtt() + if (openSuccess) { + subtitleView?.visibility = View.VISIBLE + } else { + subtitleView?.visibility = View.GONE + } + resetStatus() + } + + override fun subtitlesStateChange(toOpen: Boolean) { + super.subtitlesStateChange(toOpen) + agoraEduOptionsComponent?.hiddenRtt() + resetStatus() + if (!toOpen) { + AgoraUIToast.showDefaultToast(container.context, container.context.getString(R.string.fcr_dialog_rtt_subtitles_myself_close_finish)) + } + } + + override fun conversionStateChange(toOpen: Boolean) { + super.conversionStateChange(toOpen) + agoraEduOptionsComponent?.hiddenRtt() + resetStatus() + } + + override fun experienceInfoChange(configAllowUseRtt: Boolean, experienceDefaultTime: Int, experienceReduceTime: Int) { + super.experienceInfoChange(configAllowUseRtt, experienceDefaultTime, experienceReduceTime) + subtitleView?.setExperienceInfo(configAllowUseRtt, experienceDefaultTime, experienceReduceTime) + } + + override fun audioStateNotAllowUse() { + super.audioStateNotAllowUse() + if (rttOptionsManager.experienceReduceTimeZero()) { + subtitleView?.resetShowPosition() + subtitleView?.visibility = View.GONE + } else { + subtitleView?.resetShowPosition() + subtitleView?.visibility = View.VISIBLE + subtitleView?.setShowStatusInfo( + showProgress = false, showIcon = false, + text = container.context.getString(R.string.fcr_dialog_rtt_time_limit_status_not_allow_use) + ) + } + } + + override fun audioStateNoSpeaking() { + super.audioStateNoSpeaking() + subtitleView?.visibility = View.VISIBLE + subtitleView?.setShowStatusInfo( + showProgress = false, showIcon = false, + text = container.context.getString(R.string.fcr_dialog_rtt_subtitles_text_no_one_speaking) + ) + } + + override fun audioStateNoSpeakingMoreTime() { + super.audioStateNoSpeakingMoreTime() + subtitleView?.visibility = View.GONE + } + + override fun audioStateOpening() { + super.audioStateOpening() + subtitleView?.resetShowPosition() + subtitleView?.visibility = View.VISIBLE + subtitleView?.setShowStatusInfo( + showProgress = true, showIcon = false, + text = container.context.getString(R.string.fcr_dialog_rtt_dialog_subtitles_status_opening) + ) + } + + override fun audioStateShowSettingHint() { + super.audioStateShowSettingHint() + subtitleView?.setShowStatusInfo( + showProgress = false, showIcon = false, + text = container.context.getString(R.string.fcr_dialog_rtt_dialog_subtitles_status_opening_success_hint) + ) + } + + override fun audioStateSpeaking() { + super.audioStateSpeaking() + subtitleView?.visibility = View.VISIBLE + subtitleView?.setShowStatusInfo( + showProgress = false, showIcon = true, + text = container.context.getString(R.string.fcr_dialog_rtt_subtitles_text_listening) + ) + } + + override fun onMessageChange(currentData: RttRecordItem?) { + super.onMessageChange(currentData) + subtitleView?.visibility = View.VISIBLE + val showText = currentData?.getShowText() + subtitleView?.setShowTranslatorsInfo(currentData?.userHeader ?: "", currentData?.userName ?: "", showText!![0] ?: "", showText[1]) + } + } + + /** + * Rtt功能的管理 + */ + private val rttOptionsManager: RttOptionsManager by lazy { + RttOptionsManager(this).also { + subtitleView?.initView(it, agoraUIProvider) + it.initView(agoraUIProvider) + it.addListener(listener) + } + } + + /** + * 内容视图 + */ + private var binding: FcrOnlineToolBoxWidgetContentBinding = + FcrOnlineToolBoxWidgetContentBinding.inflate(LayoutInflater.from(container.context), container, true) + + init { + binding.agoraRttDialogLayout.clipToOutline = true + binding.agoraRttDialogSubtitles.setOnClickListener { + if (this.rttOptionsManager.isOpenSubtitles()) { + binding.agoraRttDialogSubtitlesIcon.isActivated = false + binding.agoraRttDialogSubtitlesText.setText(R.string.fcr_dialog_rtt_subtitles_close) + this.rttOptionsManager.closeSubtitles() + } else { + binding.agoraRttDialogSubtitlesIcon.isActivated = true + binding.agoraRttDialogSubtitlesText.setText(R.string.fcr_dialog_rtt_subtitles) + this.rttOptionsManager.openSubtitles() + } + } + binding.agoraRttDialogConversion.setOnClickListener { + if (this.rttOptionsManager.isOpenConversion()) { + binding.agoraRttDialogConversionIcon.isActivated = false + } else { + binding.agoraRttDialogConversionIcon.isActivated = true + } + this.rttOptionsManager.openConversion() + } + rttOptionsManager.onWidgetRoomPropertiesInit(widgetInfo?.roomProperties) + resetStatus() + } + + /** + * 重置显示状态 + */ + fun resetStatus() { + runOnUiThread { + val experienceReduceTime = rttOptionsManager.getExperienceReduceTime() + binding.agoraRttDialogSubtitlesIcon.isActivated = rttOptionsManager.isOpenSubtitles() + binding.agoraRttDialogSubtitlesText.setText(if (rttOptionsManager.isOpenSubtitles()) R.string.fcr_dialog_rtt_subtitles_close else R.string.fcr_dialog_rtt_subtitles) + binding.agoraRttDialogConversionIcon.isActivated = rttOptionsManager.isOpenConversion() + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.text = if (experienceReduceTime > 0) { + MessageFormat.format( + container.context.getString(R.string.fcr_dialog_rtt_time_limit_time), + rttOptionsManager.getExperienceDefaultTime() / 60000 + ) + } else { + container.context.getString(R.string.fcr_dialog_rtt_time_limit_end) + } + } + } + + /** + * 设置是否可以使用RTT功能 + */ + fun setAllowUse(allowUse: Boolean, reduceTime: Int) { + binding.fcrOnlineEduRttConversionDialogTimeLimitHint.visibility = if (reduceTime > 0) View.VISIBLE else View.GONE + binding.agoraRttDialogSubtitles.isEnabled = allowUse + binding.agoraRttDialogConversion.isEnabled = allowUse + binding.agoraRttDialogSubtitles.alpha = if (allowUse) 1F else 0.8F + binding.agoraRttDialogConversion.alpha = if (allowUse) 1F else 0.8F + } + + /** + * 如果体验结束的话,那么则隐藏弹窗view + */ + fun resetShowDialogIfEnd() { + if (!rttOptionsManager.isAllowUseRtt() || rttOptionsManager.experienceReduceTimeZero()) { + subtitleView?.visibility = View.GONE + } + } + + + fun dispose() { + container.removeView(binding.root) + rttOptionsManager.removeListener(listener) + } + + /** + * 获取应用实例 + */ + override fun getApplicationContext(): Context { + if (container.context is Activity) { + return container.context.applicationContext + } + return container.context + } + + /** + * 当前页面实例 + */ + override fun getActivityContext(): Context { + return container.context + } + + /** + * 判断并切换主线程 + */ + override fun runOnUiThread(runnable: Runnable) { + (getActivityContext() as Activity).runOnUiThread(runnable) + } + + /** + * widget属性信息变更 + */ + fun onWidgetRoomPropertiesUpdated( + properties: MutableMap, cause: MutableMap?, keys: MutableList, + operator: EduBaseUserInfo?, + ) { + rttOptionsManager.onWidgetRoomPropertiesUpdated(properties, operator) + } + } +} diff --git a/AgoraCloudScene/src/main/proto/RttMessage.proto b/AgoraCloudScene/src/main/proto/RttMessage.proto new file mode 100644 index 000000000..29fad5ed4 --- /dev/null +++ b/AgoraCloudScene/src/main/proto/RttMessage.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package Agora.SpeechToText; + +option objc_class_prefix = "Rtt"; + +option csharp_namespace = "AgoraRTTSample.Protobuf"; + +option java_package = "io.agora.rtc.speech2text"; +option java_outer_classname = "AgoraSpeech2TextProtobuffer"; + +message Text { + int32 vendor = 1; // Not used + int32 version = 2; // Not used + int32 seqnum = 3; // Not used + int64 uid = 4; // 转写内容对应的Rtc uid + int32 flag = 5; // Not used + int64 time = 6; // 该句转写的开始时间(只在isFinal为true时有值,其他时候为0) + int32 lang = 7; // Not used + int32 starttime = 8; // + int32 offtime = 9; // + repeated Word words = 10; // 转写的结果Array + bool end_of_segment = 11; // + int32 duration_ms = 12; // 转写消耗时间 + string data_type = 13; // transcribe(转写), translate(翻译) + repeated Translation trans = 14; // 翻译结果的Array + string culture = 15; // 该句RTT的转写目标语言 + int64 text_ts = 16; // 转写的时间戳,用于对齐翻译 +} +message Word { + string text = 1; // 转写结果 + int32 start_ms = 2; // + int32 duration_ms = 3; // + bool is_final = 4; // 是否最终结果 + double confidence = 5; // 置信度 +} +message Translation { + bool is_final = 1; // 是否最终结果 + string lang = 2; // 翻译目标语言 + repeated string texts = 3; // 翻译的结果 +} \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_active.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_active.png new file mode 100644 index 000000000..25211025d Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_active.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_active.png new file mode 100644 index 000000000..cf7550b7f Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_default.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_default.png new file mode 100644 index 000000000..dc7a34433 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_conversion_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_default.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_default.png new file mode 100644 index 000000000..b4ca3d4c6 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_active.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_active.png new file mode 100644 index 000000000..2b83bef8c Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_bg.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_bg.png new file mode 100644 index 000000000..940223b48 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_bg.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_default.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_default.png new file mode 100644 index 000000000..49790468e Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_listening.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_listening.png new file mode 100644 index 000000000..bcb8372dc Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/agora_options_icon_rtt_subtitles_listening.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png new file mode 100644 index 000000000..66ed7b535 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png new file mode 100644 index 000000000..bf0b4984f Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png new file mode 100644 index 000000000..e3197aa44 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png new file mode 100644 index 000000000..1a3574f2c Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png new file mode 100644 index 000000000..3e8018b34 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_active.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_active.png new file mode 100644 index 000000000..492b6737c Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_active.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_active.png new file mode 100644 index 000000000..2fc70841f Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_default.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_default.png new file mode 100644 index 000000000..cbdd6dbc0 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_conversion_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_default.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_default.png new file mode 100644 index 000000000..685059405 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_active.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_active.png new file mode 100644 index 000000000..d2f5c1d33 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_active.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_bg.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_bg.png new file mode 100644 index 000000000..1128b44e4 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_bg.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_default.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_default.png new file mode 100644 index 000000000..63d6605cf Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_default.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_listening.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_listening.png new file mode 100644 index 000000000..9647c68a7 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/agora_options_icon_rtt_subtitles_listening.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png new file mode 100644 index 000000000..cf4c30150 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_icon_search.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png new file mode 100644 index 000000000..4b3a7c1af Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_n.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png new file mode 100644 index 000000000..32fac6bc0 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_arrow_y.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png new file mode 100644 index 000000000..7d7d2ed57 Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_close.png differ diff --git a/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png new file mode 100644 index 000000000..6d5a3d68c Binary files /dev/null and b/AgoraCloudScene/src/main/res/drawable-xxxhdpi/fcr_online_edu_rtt_conversion_dialog_options_setting.png differ diff --git a/AgoraCloudScene/src/main/res/drawable/agora_hollow_radius_8.xml b/AgoraCloudScene/src/main/res/drawable/agora_hollow_radius_8.xml new file mode 100644 index 000000000..a691d6260 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_hollow_radius_8.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_bg.xml b/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_bg.xml new file mode 100644 index 000000000..e58f7aaaf --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_bg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_text.xml b/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_text.xml new file mode 100644 index 000000000..dc6f56f77 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_option_color_rtt_conversion_change_text.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt.xml b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt.xml new file mode 100644 index 000000000..3033e71ea --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_conversion.xml b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_conversion.xml new file mode 100644 index 000000000..422956e59 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_conversion.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_subtitles.xml b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_subtitles.xml new file mode 100644 index 000000000..41f636010 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_option_icon_rtt_subtitles.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg.xml b/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg.xml new file mode 100644 index 000000000..d3f9bb247 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg_disable.xml b/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg_disable.xml new file mode 100644 index 000000000..ea39d1638 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_options_rtt_time_limit_bg_disable.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10.xml new file mode 100644 index 000000000..91d327658 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10_top.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10_top.xml new file mode 100644 index 000000000..a7d822dcd --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_10_top.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_20.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_20.xml new file mode 100644 index 000000000..b0b04530f --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_20.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_4.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_4.xml new file mode 100644 index 000000000..d13be47ec --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_4.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_6.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_6.xml new file mode 100644 index 000000000..2380025e5 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_6.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_8.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_8.xml new file mode 100644 index 000000000..dcb6b52cd --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_8.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_max.xml b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_max.xml new file mode 100644 index 000000000..0668eab73 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/agora_solid_radius_max.xml @@ -0,0 +1,5 @@ + + + + diff --git a/AgoraCloudScene/src/main/res/drawable/fcr_online_edu_rtt_conversion_dialog_options_arrow.xml b/AgoraCloudScene/src/main/res/drawable/fcr_online_edu_rtt_conversion_dialog_options_arrow.xml new file mode 100644 index 000000000..808b3d2b8 --- /dev/null +++ b/AgoraCloudScene/src/main/res/drawable/fcr_online_edu_rtt_conversion_dialog_options_arrow.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/activity_agora_online_class.xml b/AgoraCloudScene/src/main/res/layout/activity_agora_online_class.xml index 7eaa4a3e8..54101b56b 100644 --- a/AgoraCloudScene/src/main/res/layout/activity_agora_online_class.xml +++ b/AgoraCloudScene/src/main/res/layout/activity_agora_online_class.xml @@ -89,6 +89,45 @@ + + + + + + + + + + - \ No newline at end of file + diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_edu_options_component.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_options_component.xml index 7541cf4fe..9aeb2ce85 100644 --- a/AgoraCloudScene/src/main/res/layout/fcr_online_edu_options_component.xml +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_options_component.xml @@ -31,6 +31,18 @@ android:visibility="gone" tools:visibility="visible" /> + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_conversion_dialog.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_conversion_dialog.xml new file mode 100644 index 000000000..6143dbc45 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_conversion_dialog.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_options_component.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_options_component.xml new file mode 100644 index 000000000..5816257b9 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_options_component.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_setting_dialog.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_setting_dialog.xml new file mode 100644 index 000000000..f1ff3f6b7 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_edu_rtt_setting_dialog.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_content.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_content.xml new file mode 100644 index 000000000..21c177830 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_content.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_options.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_options.xml new file mode 100644 index 000000000..d854b31ce --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_conversion_dialog_list_options.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_list.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_list.xml new file mode 100644 index 000000000..4104b2dce --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_list.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_switch.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_switch.xml new file mode 100644 index 000000000..78c3a7489 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_switch.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_title.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_title.xml new file mode 100644 index 000000000..03607da59 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_content_title.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_list_select.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_list_select.xml new file mode 100644 index 000000000..3d4c6b6b1 --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_rtt_setting_dialog_list_select.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_toast_layout_default.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_toast_layout_default.xml new file mode 100644 index 000000000..d5d01764d --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_toast_layout_default.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/layout/fcr_online_tool_box_widget_content.xml b/AgoraCloudScene/src/main/res/layout/fcr_online_tool_box_widget_content.xml new file mode 100644 index 000000000..93f80280b --- /dev/null +++ b/AgoraCloudScene/src/main/res/layout/fcr_online_tool_box_widget_content.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgoraCloudScene/src/main/res/values-zh-rTW/strings.xml b/AgoraCloudScene/src/main/res/values-zh-rTW/strings.xml index 050163315..ad3dd9018 100644 --- a/AgoraCloudScene/src/main/res/values-zh-rTW/strings.xml +++ b/AgoraCloudScene/src/main/res/values-zh-rTW/strings.xml @@ -499,4 +499,64 @@ 請先關閉分組討論 請先關閉擴展屏 + + + 字幕 + 關閉字幕 + 你已關閉字幕 + 實時轉寫 + 轉寫中... + 字幕和轉寫設置 + 聲源語言(全會統一) + 翻譯為(自己想看的語言,且只對自己可見) + 同時顯示雙語 + 確定修改 + 修改聲源語言 + 你修改教室的聲源語言為%s,將對會議所有參會者的字幕和轉寫生效。 + 限時體驗 + 限時體驗{0}分鐘 + 限時體驗結束 + 實時轉寫 + 剩余體驗時間 {0}:{1} 分鐘 + 每個房間限時{0}分鐘體驗字幕和轉寫功能,剩余 {1}:{2} 分鐘。 + 每個房間限時{0}分鐘體驗字幕和轉寫功能,體驗時間已用完。 + 無法使用 + 正在開啟字幕 + 點擊字幕位置可以更改字幕設置 + 開始轉寫 + 停止轉寫 + 當前沒有人在說話 + 正在聆聽 + + 已將聲源語言設置為 + 已開啟實時轉寫 + 已停止實時轉寫 + 開啟翻譯識別內容 + 開啟了實時轉寫服務,全體用戶可見。 + 停止了實時轉寫服務,全體用戶不可見。 + 開啟了實時轉寫服務。 + 停止了實時轉寫服務。 + 不翻譯 + 中文(繁体) + 简体中文 + 中文(繁体) + 英语(印第安语) + English + 法语 + 德语 + 泰语 + 印度语 + 印尼语 + 意大利语 + 日本語 + 韩语 + 马来西亚语 + 波斯语 + 葡萄牙语 + 俄语 + 西班牙语 + 土耳其语 + 越南语 + 忽視 + 查看 diff --git a/AgoraCloudScene/src/main/res/values-zh/strings.xml b/AgoraCloudScene/src/main/res/values-zh/strings.xml index f9f7df299..4fec9aa52 100644 --- a/AgoraCloudScene/src/main/res/values-zh/strings.xml +++ b/AgoraCloudScene/src/main/res/values-zh/strings.xml @@ -583,4 +583,64 @@ 老师放下你的手 所有学生都放下手 老师已结束投票 + + + 字幕 + 关闭字幕 + 你已关闭字幕 + 实时转写 + 转写中... + 字幕和转写设置 + 声源语言(全会统一) + 翻译为(自己想看的语言,且只对自己可见) + 同时显示双语 + 确定修改 + 修改声源语言 + 你修改本场会议的声源语言为%s,将对会议所有参会者的字幕和转写生效。 + 限时体验 + 限时体验{0}分钟 + 限时体验结束 + 实时转写 + 剩余体验时间 {0}:{1} 分钟 + 每个房间限时{0}分钟体验字幕和转写功能,剩余 {1}:{2} 分钟。 + 每个房间限时{0}分钟体验字幕和转写功能,体验时间已用完。 + 无法使用 + 正在开启字幕 + 点击字幕位置可以更改字幕设置 + 开始转写 + 停止转写 + 当前没有人在说话 + 正在聆听 + + 已将声源语言设置为 + 已开启实时转写 + 已停止实时转写 + 开启翻译识别内容 + 开启了实时转写服务,全体用户可见。 + 停止了实时转写服务,全体用户可见。 + 开启了实时转写服务。 + 停止了实时转写服务,全体用户可见 + 不翻译 + 中文(繁体) + 简体中文 + 中文(繁体) + 英语(印第安语) + English + 法语 + 德语 + 泰语 + 印度语 + 印尼语 + 意大利语 + 日本語 + 韩语 + 马来西亚语 + 波斯语 + 葡萄牙语 + 俄语 + 西班牙语 + 土耳其语 + 越南语 + 忽视 + 查看 diff --git a/AgoraCloudScene/src/main/res/values/colors.xml b/AgoraCloudScene/src/main/res/values/colors.xml index 926e34d0a..7ae01c8d6 100644 --- a/AgoraCloudScene/src/main/res/values/colors.xml +++ b/AgoraCloudScene/src/main/res/values/colors.xml @@ -22,6 +22,7 @@ #191919 #D7D7E6 #7D8798 + #CC787676 #357BF6 #C0D6FF @@ -30,6 +31,11 @@ #FE313F #FFC700 + #D9DFF7 + #373C42 + #BBBBBB + #CC787676 + #66000000 #0AFFFFFF #4D6277 @@ -42,7 +48,12 @@ #ffff0000 #357BF6 #4262FF + #1FDACF + #EEF2FA #BDBDBD + #F1F3F8 + #616161 + #959595 #357BF6 #639AFA @@ -53,6 +64,7 @@ #E2E2EE #E1EBFC #F9F9FC + #FC4C14 #191919 @@ -98,6 +110,7 @@ #FF018786 #666667 #F2F2F2 + #CCF2F2F2 #CEE3F6 #F5ECCE #fff2f2f2 @@ -129,4 +142,7 @@ #F9F9FC #E3E7EF #F5655C + #1AF5655C + + #809BFFA5 diff --git a/AgoraCloudScene/src/main/res/values/dimens.xml b/AgoraCloudScene/src/main/res/values/dimens.xml index 86b68ad89..7a5d9168b 100644 --- a/AgoraCloudScene/src/main/res/values/dimens.xml +++ b/AgoraCloudScene/src/main/res/values/dimens.xml @@ -10,6 +10,8 @@ 253dp 280dp 214dp + 180dp + 90dp 6dp 8dp @@ -45,6 +47,7 @@ 40.0000dp 44.0000dp 48.0000dp + 50.0000dp 70.0000dp 72.0000dp 88.0000dp @@ -56,9 +59,11 @@ 420.0000dp 450.0000dp 10.0000sp + 11.0000sp 12.0000sp 13.0000sp 14.0000sp + 15.0000sp 16.0000sp 17.0000sp 20.0000sp @@ -295,4 +300,8 @@ 14dp 6dp + + 490dp + 375dp + diff --git a/AgoraCloudScene/src/main/res/values/strings.xml b/AgoraCloudScene/src/main/res/values/strings.xml index 89d62d7af..d7ce981a6 100644 --- a/AgoraCloudScene/src/main/res/values/strings.xml +++ b/AgoraCloudScene/src/main/res/values/strings.xml @@ -578,4 +578,64 @@ Delete your account will result in ….\n Teacher lowers your hand All student\'s hands lowered Teacher has ended the poll + + + Subtitles + Disable subtitles + You have turned off the subtitles. + Live transcription + Transcribing + Subtitles and transliteration Settings + Original Audio + Translation Display + Bilingual Mode + Confirm + Change source language + You are changing the source language of this Room to %s.This will affect captions and transcripts for all attendees + Limited time experience + {0} Limited time experience + Limited time experience ended + Live transcription + remaining experience time {0}:{1} + Each room has a {0} minute limit to experience subtitles and transcription features, with {1}:{2} minutes remaining. + Each room has a {0} minute limit to experience subtitles and transcription features, and the experience time has run out. + Unavailable + Turning on subtitles + Click on the subtitle position to change subtitle settings. + Transcribe + End Transcript + No one is currently speaking + Listening + Me + changed the source language for captions and transcription to + Turned on live transcription + Turned off live transcription + Enable translation of recognized content + The live transcription service has been activated and is visible to all users. + The live transcription service has been stopped and is invisible to all users. + Turned on live transcription + Turned off live transcription + Do Not Translate + 中文(繁体) + 简体中文 + 中文(繁体) + 英语(印第安语) + English + 法语 + 德语 + 泰语 + 印度语 + 印尼语 + 意大利语 + 日本語 + 韩语 + 马来西亚语 + 波斯语 + 葡萄牙语 + 俄语 + 西班牙语 + 土耳其语 + 越南语 + Ignore + View diff --git a/app/src/main/java/io/agora/education/EduApplication.java b/app/src/main/java/io/agora/education/EduApplication.java index d3641c721..0b1262d51 100644 --- a/app/src/main/java/io/agora/education/EduApplication.java +++ b/app/src/main/java/io/agora/education/EduApplication.java @@ -54,7 +54,7 @@ public void onCreate() { // } // TODO test XXX - // setTestDev(); +// setTestDev(); setDevHost(); setDarkMode(); diff --git a/build.gradle b/build.gradle index 3559663bc..f1d925881 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.19' } } diff --git a/config.gradle b/config.gradle index 512d0192a..7be2ac76c 100644 --- a/config.gradle +++ b/config.gradle @@ -2,7 +2,7 @@ ext { //def fcrApaasVersion = "2.8.0" sdkVersion = [// TODO versionName 如果以'-SNAPSHOT'结尾,aar将会被发送到SNAPSHOT仓库 - 'appVersion':"2.8.102" + 'appVersion':"2.8.111-SNAPSHOT" ] maven = [