diff --git a/README.md b/README.md
index 323e67b..39933cf 100644
--- a/README.md
+++ b/README.md
@@ -79,19 +79,19 @@ This demo support Android device with **Android 7.0** or later
- For Full SDK
```
dependencies {
- implementation 'com.ciscowebex:webexsdk:3.9.2'
+ implementation 'com.ciscowebex:webexsdk:3.10.0'
}
```
- For Meeting SDK
```
dependencies {
- implementation 'com.ciscowebex:webexsdk-meeting:3.9.2'
+ implementation 'com.ciscowebex:webexsdk-meeting:3.10.0'
}
```
- For WebexCalling SDK
```
dependencies {
- implementation 'com.ciscowebex:webexsdk-wxc:3.9.2'
+ implementation 'com.ciscowebex:webexsdk-wxc:3.10.0'
}
```
diff --git a/app/build.gradle b/app/build.gradle
index 74bdef8..091e162 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -32,8 +32,8 @@ android {
applicationId "com.cisco.sdk_android"
minSdkVersion Versions.minSdk
targetSdkVersion Versions.targetSdk
- versionCode 3090200
- versionName "3.9.2"
+ versionCode 3100000
+ versionName "3.10.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -113,11 +113,12 @@ android {
}
dependencies {
+
+ //At a time only one WebexSDK should be used.
+ implementation 'com.ciscowebex:webexsdk:3.10.0' // For full flavor
+ //implementation 'com.ciscowebex:webexsdk-wxc:3.10.0' //For webexCalling flavor
+ //implementation 'com.ciscowebex:webexsdk-meeting:3.10.0' // For meeting flavor
- // At a time only one WebexSDK should be used.
- implementation 'com.ciscowebex:webexsdk:3.9.2' // For full flavor
-// implementation 'com.ciscowebex:webexsdk-wxc:3.9.2' //For webexCalling flavor
- //implementation 'com.ciscowebex:webexsdk-meeting:3.9.2' // For meeting flavor
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation Dependencies.kotlinStdLib
implementation Dependencies.coreKtx
@@ -155,5 +156,4 @@ dependencies {
implementation Dependencies.firebaseCrashlytics
implementation Dependencies.gson
implementation Dependencies.glide
-
}
diff --git a/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkTest.kt b/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkTest.kt
index 2f25e77..489f4a2 100644
--- a/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkTest.kt
+++ b/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkTest.kt
@@ -10,6 +10,7 @@ import androidx.annotation.StringRes
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 84cb6c1..ceef1b5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -76,8 +76,7 @@
android:name=".calling.LockScreenActivity"
android:showOnLockScreen="true"
android:showWhenLocked="true"
- android:turnScreenOn="true"
- />
+ android:turnScreenOn="true" />
+
+
-
+ android:name="android.content.APP_RESTRICTIONS"
+ android:resource="@xml/app_restrictions" />
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt
index 9905b87..5cc336b 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt
@@ -19,7 +19,18 @@ import com.ciscowebex.androidsdk.membership.Membership
import com.ciscowebex.androidsdk.membership.MembershipObserver
import com.ciscowebex.androidsdk.message.LocalFile
import com.ciscowebex.androidsdk.message.MessageObserver
-import com.ciscowebex.androidsdk.phone.*
+import com.ciscowebex.androidsdk.phone.Breakout
+import com.ciscowebex.androidsdk.phone.BreakoutSession
+import com.ciscowebex.androidsdk.phone.Call
+import com.ciscowebex.androidsdk.phone.CallMembership
+import com.ciscowebex.androidsdk.phone.MediaOption
+import com.ciscowebex.androidsdk.phone.Phone
+import com.ciscowebex.androidsdk.phone.VirtualBackground
+import com.ciscowebex.androidsdk.phone.CallObserver
+import com.ciscowebex.androidsdk.phone.NotificationCallType
+import com.ciscowebex.androidsdk.phone.ReceivingNoiseInfo
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
import com.ciscowebex.androidsdk.space.SpaceObserver
import java.io.PrintWriter
@@ -660,6 +671,24 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate {
}
}
}
+
+ override fun onClosedCaptionsArrived(closedCaptions: CaptionItem) {
+ val observers: MutableList? = _callObservers[_callId]
+ observers?.let { it ->
+ it.forEach { observer ->
+ observer.onClosedCaptionsArrived(closedCaptions)
+ }
+ }
+ }
+
+ override fun onClosedCaptionsInfoChanged(closedCaptionsInfo: ClosedCaptionsInfo) {
+ val observers: MutableList? = _callObservers[_callId]
+ observers?.let { it ->
+ it.forEach { observer ->
+ observer.onClosedCaptionsInfoChanged(closedCaptionsInfo)
+ }
+ }
+ }
}
private fun registerCallObserver(call: Call) {
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt
index 1975eba..2a56ec5 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt
@@ -19,8 +19,10 @@ import org.json.JSONObject
import com.ciscowebex.androidsdk.auth.PhoneServiceRegistrationFailureReason
import com.ciscowebex.androidsdk.auth.TokenAuthenticator
import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus
+import com.ciscowebex.androidsdk.internal.ResultImpl
import com.ciscowebex.androidsdk.kitchensink.calling.CallObserverInterface
import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage
+import com.ciscowebex.androidsdk.kitchensink.utils.Constants
import com.ciscowebex.androidsdk.message.LocalFile
import com.ciscowebex.androidsdk.phone.ShareConfig
import com.ciscowebex.androidsdk.phone.BreakoutSession.BreakoutSessionError
@@ -45,6 +47,8 @@ import com.ciscowebex.androidsdk.phone.SwitchToAudioVideoCallResult
import com.ciscowebex.androidsdk.phone.PhoneConnectionResult
import com.ciscowebex.androidsdk.phone.ReceivingNoiseInfo
import com.ciscowebex.androidsdk.phone.ReceivingNoiseRemovalEnableResult
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
import com.google.firebase.installations.FirebaseInstallations
import java.io.PrintWriter
@@ -541,6 +545,14 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi
override fun onReceivingNoiseInfoChanged(info: ReceivingNoiseInfo) {
callObserverInterface?.onReceivingNoiseInfoChanged(info)
}
+
+ override fun onClosedCaptionsArrived(closedCaptions: CaptionItem) {
+ callObserverInterface?.onClosedCaptionsArrived(closedCaptions)
+ }
+
+ override fun onClosedCaptionsInfoChanged(closedCaptionsInfo: ClosedCaptionsInfo) {
+ callObserverInterface?.onClosedCaptionsInfoChanged(closedCaptionsInfo)
+ }
}
val callObserverMap : HashMap = HashMap()
@@ -756,7 +768,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi
fun setPushTokens(id: String, token: String){
if(BuildConfig.WEBHOOK_URL.isEmpty()) {
- webex.phone.setPushTokens(KitchenSinkApp.applicationContext().packageName, id, token)
+ webex.phone.setPushTokens(KitchenSinkApp.applicationContext().packageName, id, token, Constants.Keys.appId)
}
}
@@ -857,8 +869,20 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi
?.getValue() as Boolean?
}
- fun switchAudioMode(mode: Call.AudioOutputMode) {
- getCall(currentCallId.orEmpty())?.switchAudioOutput(mode)
+ fun switchAudioMode(mode: Call.AudioOutputMode, handler: CompletionHandler) {
+ getCall(currentCallId.orEmpty())?.switchAudioOutput(mode) { result ->
+ if (result.data == false) {
+ Log.d(tag, "ATSDK Error: Switch mode to ${mode.name} failed")
+ handler.onComplete(ResultImpl.success(false))
+ } else {
+ Log.d(tag, "ATSDK Switch mode to ${mode.name} success")
+ handler.onComplete(ResultImpl.success(true))
+ }
+ }
+ }
+
+ fun getCurrentAudioOutputMode(): Call.AudioOutputMode? {
+ return getCall(currentCallId.orEmpty())?.getCurrentAudioOutput()
}
fun enableAudioBNR(value: Boolean) {
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallActivity.kt
index a9d4a8f..20ac11c 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallActivity.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallActivity.kt
@@ -29,6 +29,8 @@ import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityCallBinding
import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage
import com.ciscowebex.androidsdk.kitchensink.utils.Constants
import com.ciscowebex.androidsdk.phone.*
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -459,6 +461,14 @@ class CallActivity : BaseActivity(), CallControlsFragment.OnCallActionListener,
TODO("Not yet implemented")
}
+ override fun onClosedCaptionsArrived(closedCaptions: CaptionItem) {
+ TODO("Not yet implemented")
+ }
+
+ override fun onClosedCaptionsInfoChanged(closedCaptionsInfo: ClosedCaptionsInfo) {
+ TODO("Not yet implemented")
+ }
+
override fun finish() {
if(calls.size > 0){
//Resume a queued call
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt
index 7cf0708..53ab424 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt
@@ -26,7 +26,8 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni
val cameraOptionsClickListener: (Call?) -> Unit,
val multiStreamOptionsClickListener: (Call?) -> Unit,
val sendDTMFClickListener: (Call?) -> Unit,
- val showBreakoutSessions: () -> Unit): BottomSheetDialogFragment() {
+ val showBreakoutSessions: () -> Unit,
+ val closedCaptionOptions: (Call?) -> Unit): BottomSheetDialogFragment() {
companion object {
val TAG = "CallBottomSheetFragment"
}
@@ -215,6 +216,11 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni
showBreakoutSessions()
}
+ closedCaptionOptions.setOnClickListener {
+ dismiss()
+ closedCaptionOptions(call)
+ }
+
cancel.setOnClickListener { dismiss() }
}.root
}
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt
index fae6e60..28c16f1 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt
@@ -46,6 +46,11 @@ import com.ciscowebex.androidsdk.WebexError
import com.ciscowebex.androidsdk.kitchensink.R
import com.ciscowebex.androidsdk.kitchensink.WebexRepository
import com.ciscowebex.androidsdk.kitchensink.WebexViewModel
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.ClosedCaptionsController
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.ClosedCaptionsViewModel
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.LanguageData
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.REQUEST_CODE_SPOKEN_LANGUAGE
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.REQUEST_CODE_TRANSLATION_LANGUAGE
import com.ciscowebex.androidsdk.kitchensink.calling.participants.ParticipantsFragment
import com.ciscowebex.androidsdk.kitchensink.calling.transcription.TranscriptionsDialogFragment
import com.ciscowebex.androidsdk.kitchensink.databinding.DialogEnterMeetingPinBinding
@@ -56,6 +61,7 @@ import com.ciscowebex.androidsdk.kitchensink.setup.BackgroundOptionsBottomSheetF
import com.ciscowebex.androidsdk.kitchensink.utils.AudioManagerUtils
import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage
import com.ciscowebex.androidsdk.kitchensink.utils.Constants
+import com.ciscowebex.androidsdk.kitchensink.utils.Constants.Intent.CLOSED_CAPTION_LANGUAGE_ITEM
import com.ciscowebex.androidsdk.kitchensink.utils.extensions.toast
import com.ciscowebex.androidsdk.kitchensink.utils.showDialogForDTMF
import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage
@@ -80,18 +86,21 @@ import com.ciscowebex.androidsdk.phone.Breakout
import com.ciscowebex.androidsdk.phone.BreakoutSession
import com.ciscowebex.androidsdk.phone.ReceivingNoiseInfo
import com.ciscowebex.androidsdk.phone.ShareConfig
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
import org.koin.android.ext.android.inject
import com.ciscowebex.androidsdk.utils.internal.MimeUtils
+import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.util.Date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import org.koin.android.ext.android.bind
import org.koin.androidx.viewmodel.ext.android.viewModel
class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface {
private val TAG = "CallControlsFragment"
val webexViewModel: WebexViewModel by viewModel()
+ val captionsViewModel: ClosedCaptionsViewModel by viewModel()
private lateinit var binding: FragmentCallControlsBinding
private var callFailed = false
private var isIncomingActivity = false
@@ -109,8 +118,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
private lateinit var mediaStreamBottomSheetFragment: MediaStreamBottomSheetFragment
private lateinit var photoViewerBottomSheetFragment: PhotoViewerBottomSheetFragment
private lateinit var breakoutSessionBottomSheetFragment: BreakoutSessionsBottomSheetFragment
+ private lateinit var switchAudioBottomSheetFragment: SwitchAudioBottomSheetFragment
private lateinit var incomingInfoAdapter: IncomingCallBottomSheetFragment.IncomingInfoAdapter
private lateinit var breakoutSessionsAdapter: BreakoutSessionsBottomSheetFragment.BreakoutSessionsAdapter
+ private lateinit var captionsController: ClosedCaptionsController
private val mAuxStreamViewMap: HashMap = HashMap()
private var callerId: String = ""
var bottomSheetFragment: BackgroundOptionsBottomSheetFragment? = null
@@ -786,6 +797,28 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
}
}
+ override fun onClosedCaptionsArrived(captions: CaptionItem) {
+ CoroutineScope(Dispatchers.Main).launch {
+ captionsController.showCaptionView(binding.rootLayout, captions)
+ if(captions.isFinal) {
+ captionsViewModel.updateData(captions)
+ Log.d(TAG, " Captions are arrived from ${captions.getDisplayName()}")
+ }
+ }
+ }
+
+ override fun onClosedCaptionsInfoChanged(closedCaptionsInfo: ClosedCaptionsInfo) {
+ Log.d(
+ TAG,
+ " Captions Info changed: current spkn: ${
+ closedCaptionsInfo.getCurrentSpokenLanguage().getLanguageTitle()
+ } and trns ${closedCaptionsInfo.getCurrentTranslationLanguage().getLanguageTitle()}"
+ )
+ captionsController.setLanguages(requireContext(), closedCaptionsInfo) { intent, code ->
+ startActivityForResult(intent, code)
+ }
+ }
+
@SuppressLint("NotifyDataSetChanged")
private fun observerCallLiveData() {
@@ -882,6 +915,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
dismissErrorDialog()
}
}
+
+ call?.let {
+ captionsController = ClosedCaptionsController(call)
+ }
}
})
@@ -1351,21 +1388,22 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
}
callOptionsBottomSheetFragment = CallBottomSheetFragment(
- { call -> showIncomingCallBottomSheet()},
- { call -> showTranscriptions(call) },
- { call -> toggleWXAClickListener(call) },
- { call -> receivingVideoListener(call) },
- { call -> receivingAudioListener(call) },
- { call -> receivingSharingListener(call) },
- { call -> scalingModeClickListener(call) },
- { call -> virtualBackgroundOptionsClickListener(call) },
- { call -> compositeStreamLayoutClickListener(call) },
- { call -> swapVideoClickListener(call) },
- { call -> forceLandscapeClickListener(call) },
- { call -> cameraOptionsClickListener(call) },
- { call -> multiStreamOptionsClickListener(call) },
- { call -> sendDTMFClickListener(call) },
- { showBreakoutSessions()})
+ { call -> showIncomingCallBottomSheet()},
+ { call -> showTranscriptions(call) },
+ { call -> toggleWXAClickListener(call) },
+ { call -> receivingVideoListener(call) },
+ { call -> receivingAudioListener(call) },
+ { call -> receivingSharingListener(call) },
+ { call -> scalingModeClickListener(call) },
+ { call -> virtualBackgroundOptionsClickListener(call) },
+ { call -> compositeStreamLayoutClickListener(call) },
+ { call -> swapVideoClickListener(call) },
+ { call -> forceLandscapeClickListener(call) },
+ { call -> cameraOptionsClickListener(call) },
+ { call -> multiStreamOptionsClickListener(call) },
+ { call -> sendDTMFClickListener(call) },
+ { showBreakoutSessions() },
+ { call -> showCaptionDialog(call) })
multiStreamOptionsBottomSheetFragment = MultiStreamOptionsBottomSheetFragment({ call -> setCategoryAOptionClickListener(call) },
{ call -> setCategoryBOptionClickListener(call) },
@@ -1400,6 +1438,9 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
photoViewerBottomSheetFragment = PhotoViewerBottomSheetFragment()
+ switchAudioBottomSheetFragment = SwitchAudioBottomSheetFragment({toggleAudioMode(AudioMode.EARPIECE)},
+ {toggleAudioMode(AudioMode.SPEAKER)}, {toggleAudioMode(AudioMode.BLUETOOTH)}, {toggleAudioMode(AudioMode.WIRED_HEADSET)})
+
callingActivity = bundle?.getInt(Constants.Intent.CALLING_ACTIVITY_ID, 0)!!
val incomingCallId = bundle.getString(Constants.Intent.CALL_ID) ?: ""
if (callingActivity == 1) {
@@ -1461,7 +1502,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
binding.ibMute.setOnClickListener(this)
binding.ibParticipants.setOnClickListener(this)
- binding.ibSpeaker.setOnClickListener(this)
+ binding.ibAudioMode.setOnClickListener(this)
binding.ibAdd.setOnClickListener(this)
binding.ibTransferCall.setOnClickListener(this)
binding.ibDirecttransferCall.setOnClickListener(this)
@@ -1487,6 +1528,12 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
passwordDialog = Dialog(requireContext())
}
+ private fun showCaptionDialog(call: Call?) {
+ captionsController.showCaptionDialog(requireContext(), call) {intent, code ->
+ startActivityForResult(intent, code)
+ }
+ }
+
private fun showBreakoutSessions() {
breakoutSessionsAdapter.sessions = breakoutSessions.toMutableList()
breakoutSessionBottomSheetFragment.adapter = breakoutSessionsAdapter
@@ -1496,6 +1543,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
fun answerCall(call: Call) {
webexViewModel.answer(call, getMediaOption())
+ captionsController = ClosedCaptionsController(call)
}
private fun initIncomingCallBottomSheet() {
@@ -1519,8 +1567,8 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
val dialog = ParticipantsFragment.newInstance(callId)
dialog.show(childFragmentManager, ParticipantsFragment::javaClass.name)
}
- binding.ibSpeaker -> {
- toggleSpeaker(v)
+ binding.ibAudioMode -> {
+ switchAudioBottomSheetFragment.show(childFragmentManager, SwitchAudioBottomSheetFragment::javaClass.name)
}
binding.ibAdd -> {
//while associating a call, existing call needs to be put on hold
@@ -2077,9 +2125,19 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
screenShareButtonVisibilityState()
directTransferButtonStateUpdate()
videoViewTextColorState(webexViewModel.isRemoteVideoMuted)
-
+ updateAudioModeButton()
}
+ }
+ }
+ private fun updateAudioModeButton() {
+ webexViewModel.getCurrentAudioOutputMode()?.let { outputMode ->
+ when (outputMode) {
+ Call.AudioOutputMode.PHONE -> binding.ibAudioMode.setImageResource(R.drawable.ic_earpiece)
+ Call.AudioOutputMode.SPEAKER -> binding.ibAudioMode.setImageResource(R.drawable.ic_speaker)
+ Call.AudioOutputMode.BLUETOOTH_HEADSET -> binding.ibAudioMode.setImageResource(R.drawable.ic_bluetooth)
+ Call.AudioOutputMode.HEADSET -> binding.ibAudioMode.setImageResource(R.drawable.ic_headset)
+ }
}
}
@@ -2302,24 +2360,42 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
return status
}
- private fun toggleSpeaker(v: View) {
- v.isSelected = !v.isSelected
- when {
- v.isSelected -> {
- webexViewModel.switchAudioMode(Call.AudioOutputMode.SPEAKER)
+ private fun toggleAudioMode(mode: AudioMode) {
+ when (mode) {
+ AudioMode.SPEAKER -> {
+ webexViewModel.switchAudioMode(Call.AudioOutputMode.SPEAKER) {
+ if (it.data == true)
+ binding.ibAudioMode.setImageResource(R.drawable.ic_speaker)
+ }
}
- audioManagerUtils?.isBluetoothHeadsetConnected == true -> {
- webexViewModel.switchAudioMode(Call.AudioOutputMode.BLUETOOTH_HEADSET)
+ AudioMode.BLUETOOTH -> {
+ webexViewModel.switchAudioMode(Call.AudioOutputMode.BLUETOOTH_HEADSET) {
+ if (it.data == true)
+ binding.ibAudioMode.setImageResource(R.drawable.ic_bluetooth)
+ }
}
- audioManagerUtils?.isWiredHeadsetOn == true -> {
- webexViewModel.switchAudioMode(Call.AudioOutputMode.HEADSET)
+ AudioMode.EARPIECE -> {
+ webexViewModel.switchAudioMode(Call.AudioOutputMode.PHONE) {
+ if (it.data == true)
+ binding.ibAudioMode.setImageResource(R.drawable.ic_earpiece)
+ }
}
- else -> {
- webexViewModel.switchAudioMode(Call.AudioOutputMode.PHONE)
+ AudioMode.WIRED_HEADSET -> {
+ webexViewModel.switchAudioMode(Call.AudioOutputMode.HEADSET) {
+ if (it.data == true)
+ binding.ibAudioMode.setImageResource(R.drawable.ic_headset)
+ }
}
}
}
+ enum class AudioMode {
+ SPEAKER,
+ EARPIECE,
+ BLUETOOTH,
+ WIRED_HEADSET
+ }
+
internal fun handleFCMIncomingCall(callId: String) {
mHandler.post {
webexViewModel.setFCMIncomingListenerObserver(callId)
@@ -2599,9 +2675,18 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
val callNumber = data?.getStringExtra(CALLER_ID) ?: ""
//start call association to add new person on call
startAssociatedCall(callNumber, CallAssociationType.Transfer, true)
- }else if (requestCode == REQUEST_CODE_BLINDTRANSFER && resultCode == Activity.RESULT_OK){
+ } else if (requestCode == REQUEST_CODE_BLINDTRANSFER && resultCode == Activity.RESULT_OK) {
val callNumber = data?.getStringExtra(CALLER_ID) ?: ""
directTransferCall(callNumber)
+ } else if (
+ (requestCode == REQUEST_CODE_SPOKEN_LANGUAGE || requestCode == REQUEST_CODE_TRANSLATION_LANGUAGE) &&
+ resultCode == Activity.RESULT_OK
+ ) {
+ captionsController.handleLanguageSelection(
+ requireContext(),
+ requestCode,
+ data?.getParcelableExtra(CLOSED_CAPTION_LANGUAGE_ITEM)
+ )
}
}
@@ -3002,7 +3087,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
binding.optionButtonsContainer.visibility = currentView
binding.ibMute.visibility = currentView
binding.ibHoldCall.visibility = currentView
- binding.ibSpeaker.visibility = currentView
+ binding.ibAudioMode.visibility = currentView
binding.controlsRow2.visibility = currentView
binding.ibVideo.visibility = currentView
binding.ibParticipants.visibility = currentView
@@ -3019,7 +3104,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface
}
binding.btnReturnToMainSession.visibility = returnToMainSessionVisibility
}
-
+
fun aspectRatio(): Rational {
var width = binding.videoCallLayout.width.toInt()
var height = binding.videoCallLayout.height.toInt()
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallModule.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallModule.kt
index 7c61ecd..3f7918c 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallModule.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallModule.kt
@@ -1,10 +1,16 @@
package com.ciscowebex.androidsdk.kitchensink.calling
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.ClosedCaptionsRepository
+import com.ciscowebex.androidsdk.kitchensink.calling.captions.ClosedCaptionsViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val callModule = module {
viewModel {
CallViewModel(get())
+ ClosedCaptionsViewModel(get())
}
+
+ single { ClosedCaptionsRepository() }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt
index 3167050..de3d588 100644
--- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt
@@ -6,6 +6,8 @@ import com.ciscowebex.androidsdk.phone.Call
import com.ciscowebex.androidsdk.phone.CallObserver
import com.ciscowebex.androidsdk.phone.BreakoutSession.BreakoutSessionError
import com.ciscowebex.androidsdk.phone.ReceivingNoiseInfo
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
/*
* This interface is written to overcome the limitation of live data postValue.
@@ -41,4 +43,8 @@ interface CallObserverInterface {
fun onBreakoutUpdated(breakout: Breakout)
fun onBreakoutError(error: BreakoutSessionError)
fun onReceivingNoiseInfoChanged(info: ReceivingNoiseInfo)
+
+ // Closedcaption
+ fun onClosedCaptionsArrived(closedCaptions: CaptionItem)
+ fun onClosedCaptionsInfoChanged(closedCaptionsInfo: ClosedCaptionsInfo)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/SwitchAudioFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/SwitchAudioFragment.kt
new file mode 100644
index 0000000..d97bc01
--- /dev/null
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/SwitchAudioFragment.kt
@@ -0,0 +1,44 @@
+package com.ciscowebex.androidsdk.kitchensink.calling
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetSwitchAudioOptionsBinding
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+class SwitchAudioBottomSheetFragment(val onEarpieceSelected: () -> Unit,
+ val onSpeakerSelected: () -> Unit,
+ val onBluetoothSelected: () -> Unit,
+ val onHeadsetSelected: () -> Unit): BottomSheetDialogFragment() {
+ companion object {
+ val TAG = "SwitchAudioBottomSheetFragment"
+ }
+
+ private lateinit var binding: BottomSheetSwitchAudioOptionsBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return BottomSheetSwitchAudioOptionsBinding.inflate(inflater, container, false).also { binding = it }.apply {
+
+ selectSpeakerOption.setOnClickListener {
+ dismiss()
+ onSpeakerSelected()
+ }
+
+ selectBluetooth.setOnClickListener {
+ dismiss()
+ onBluetoothSelected()
+ }
+
+ selectPhoneEarpieceOption.setOnClickListener {
+ dismiss()
+ onEarpieceSelected()
+ }
+
+ selectHeadset.setOnClickListener {
+ dismiss()
+ onHeadsetSelected()
+ }
+ }.root
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/CaptionData.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/CaptionData.kt
new file mode 100644
index 0000000..da14e98
--- /dev/null
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/CaptionData.kt
@@ -0,0 +1,34 @@
+package com.ciscowebex.androidsdk.kitchensink.calling.captions
+
+import android.os.Parcel
+import android.os.Parcelable
+
+data class CaptionData(val name:String?, val timestamp:String?, val content:String?):Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readString(),
+ parcel.readString(),
+ parcel.readString()
+ ) {
+ }
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString(name)
+ parcel.writeString(timestamp)
+ parcel.writeString(content)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): CaptionData {
+ return CaptionData(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsActivity.kt
new file mode 100644
index 0000000..68b8f3b
--- /dev/null
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsActivity.kt
@@ -0,0 +1,82 @@
+package com.ciscowebex.androidsdk.kitchensink.calling.captions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.ciscowebex.androidsdk.kitchensink.BaseActivity
+import com.ciscowebex.androidsdk.kitchensink.R
+import com.ciscowebex.androidsdk.kitchensink.utils.Constants
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import java.util.ArrayList
+import java.util.Collections
+
+class ClosedCaptionsActivity : BaseActivity() {
+
+ val captionsViewModel: ClosedCaptionsViewModel by viewModel()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_closed_captions)
+ val recyclerView = findViewById(R.id.ccRecyclerView)
+ findViewById(R.id.title).text = getString(R.string.closed_captions)
+
+ var captions = intent?.getParcelableArrayListExtra(Constants.Intent.CLOSED_CAPTION_DATA)
+ captionsViewModel.captions.observe(this@ClosedCaptionsActivity) { caption ->
+ if (captions == null) {
+ captions = ArrayList(1)
+ updateViews(captions)
+ }
+
+ captions?.let {
+ if (it[it.size - 1] != caption)
+ it.add(caption)
+ recyclerView.adapter?.let { recyclerView ->
+ recyclerView.notifyItemInserted(recyclerView.itemCount - 1)
+ }
+ }
+ }
+ updateViews(captions)
+ }
+
+ fun updateViews(captions: ArrayList?) {
+ val recyclerView = findViewById(R.id.ccRecyclerView)
+ val noDataView = findViewById(R.id.ccNoData)
+ if (captions.isNullOrEmpty()) {
+ noDataView.visibility = View.VISIBLE
+ recyclerView.visibility = View.INVISIBLE
+ } else {
+ noDataView.visibility = View.INVISIBLE
+ recyclerView.visibility = View.VISIBLE
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ recyclerView.adapter = CaptionAdapter(captions)
+ }
+ }
+
+ class CaptionAdapter(private val captions: ArrayList) :
+ RecyclerView.Adapter() {
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val primaryTextView: TextView = itemView.findViewById(R.id.primaryText)
+ val subTextView: TextView = itemView.findViewById(R.id.subText)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.list_item_closed_caption, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val caption = captions[position]
+ holder.primaryTextView.text = "${caption.name} ${caption.timestamp}"
+ holder.subTextView.text = caption.content
+ }
+
+ override fun getItemCount(): Int {
+ return captions.size
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsController.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsController.kt
new file mode 100644
index 0000000..d3cb52b
--- /dev/null
+++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/captions/ClosedCaptionsController.kt
@@ -0,0 +1,242 @@
+package com.ciscowebex.androidsdk.kitchensink.calling.captions
+
+import android.app.Dialog
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.Switch
+import android.widget.TextView
+import android.widget.Toast
+import com.ciscowebex.androidsdk.kitchensink.R
+import com.ciscowebex.androidsdk.kitchensink.utils.Constants
+import com.ciscowebex.androidsdk.phone.Call
+import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem
+import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo
+import com.ciscowebex.androidsdk.phone.closedCaptions.LanguageItem
+import com.google.android.material.snackbar.Snackbar
+
+const val REQUEST_CODE_SPOKEN_LANGUAGE = 1214
+const val REQUEST_CODE_TRANSLATION_LANGUAGE = 1215
+
+class ClosedCaptionsController(var call: Call?) {
+
+ private val TAG = "ClosedCaptionsController"
+ private lateinit var dialog: Dialog
+ private var snackbar: Snackbar? = null
+
+ private fun getParceableLanguages(languages: List?): ArrayList {
+ return languages?.mapNotNull { languageItem ->
+ LanguageData(
+ title = languageItem.getLanguageTitle(),
+ titleInEnglish = languageItem.getLanguageTitleInEnglish(),
+ code = languageItem.getLanguageCode()
+ )
+ }?.toCollection(ArrayList()) ?: ArrayList()
+
+ }
+
+ private fun getParceableCaptions(captions: List?): ArrayList {
+ return captions?.mapNotNull { captionItem ->
+ CaptionData(
+ name = captionItem.getDisplayName(),
+ timestamp = captionItem.getTimeStamp(),
+ content = captionItem.getContent()
+ )
+ }?.toCollection(ArrayList()) ?: ArrayList()
+ }
+
+ fun showCaptionDialog(
+ context: Context,
+ data: Call?,
+ startLanguageActivityForResult: (intent: Intent, code: Int) -> Unit
+ ) {
+ call = data // update the call object , as this class will hold the current active call.
+ val isCaptionsEnabled = call?.isClosedCaptionEnabled ?: false
+ dialog = Dialog(context)
+ dialog.setTitle(context.getString(R.string.cc_title))
+ dialog.setCancelable(true)
+ dialog.setContentView(R.layout.closed_caption_options_dialog)
+
+ //caption enabled/disabled
+ val ccSwitch = dialog.findViewById(R.id.ccSwitch)
+ ccSwitch.isChecked = isCaptionsEnabled
+ ccSwitch.setOnCheckedChangeListener { _, isChecked ->
+ call?.toggleClosedCaption(isChecked) {
+ Log.d(TAG, "Caption state toggled to: ${it.data}")
+ if (isChecked) {
+ setLanguages(
+ context,
+ call?.getClosedCaptionsInfo(),
+ startLanguageActivityForResult
+ )
+ }
+ }
+ }
+
+ // language for captions
+ setLanguages(context, call?.getClosedCaptionsInfo(), startLanguageActivityForResult)
+
+ // View all captions
+ dialog.findViewById(R.id.ccShowCaptions).setOnClickListener {
+ if(shouldDisableCLick(dialog)) return@setOnClickListener
+
+ val closedCaptions = call?.getClosedCaptions()
+ val intent = Intent(context, ClosedCaptionsActivity::class.java)
+ intent.putParcelableArrayListExtra(
+ Constants.Intent.CLOSED_CAPTION_DATA,
+ getParceableCaptions(closedCaptions)
+ )
+ context.startActivity(intent)
+ }
+
+ //Close action
+ dialog.findViewById