From 29acd35fcf70e3f1a58708908f630e83cc99bf13 Mon Sep 17 00:00:00 2001 From: Namratha A Date: Tue, 20 Aug 2024 16:41:00 +0530 Subject: [PATCH 1/3] 3.13 release --- README.md | 6 +- app/build.gradle | 6 +- app/proguard-rules.pro | 7 +- .../teams/membership/TeamFragmentTest.kt | 14 +- app/src/main/AndroidManifest.xml | 5 + .../androidsdk/kitchensink/WebexRepository.kt | 7 +- .../androidsdk/kitchensink/WebexViewModel.kt | 47 ++++++ .../calling/CallBottomSheetFragment.kt | 8 +- .../calling/CallControlsFragment.kt | 134 ++++++++++++++++-- .../res/layout/bottom_sheet_call_options.xml | 24 +++- .../res/layout/fragment_call_controls.xml | 2 +- app/src/main/res/values/strings.xml | 2 + 12 files changed, 235 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d72a4c1..5fd5b69 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.12.0' + implementation 'com.ciscowebex:webexsdk:3.13.0' } ``` - For Meeting SDK ``` dependencies { - implementation 'com.ciscowebex:webexsdk-meeting:3.12.0' + implementation 'com.ciscowebex:webexsdk-meeting:3.13.0' } ``` - For WebexCalling SDK ``` dependencies { - implementation 'com.ciscowebex:webexsdk-wxc:3.12.0' + implementation 'com.ciscowebex:webexsdk-wxc:3.13.0' } ``` diff --git a/app/build.gradle b/app/build.gradle index f8ae791..1ceacd6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,9 +121,9 @@ android { dependencies { //At a time only one WebexSDK should be used. - implementation 'com.ciscowebex:webexsdk:3.12.0' // For full flavor - //implementation 'com.ciscowebex:webexsdk-wxc:3.12.0' //For webexCalling flavor - //implementation 'com.ciscowebex:webexsdk-meeting:3.12.0' // For meeting flavor + implementation 'com.ciscowebex:webexsdk:3.13.0' // For full flavor + //implementation 'com.ciscowebex:webexsdk-wxc:3.13.0' //For webexCalling flavor + //implementation 'com.ciscowebex:webexsdk-meeting:3.13.0' // For meeting flavor implementation fileTree(dir: "libs", include: ["*.jar"]) implementation Dependencies.kotlinStdLib diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7f75e3b..e909b57 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -112,14 +112,17 @@ *; } --keep public enum com.webex.scf.commonhead.models.**{ +-keep public enum com.ciscowebex.androidsdk.omniusenums.**{ *; } --keep public class com.webex.scf.commonhead.models.**{ +-keep public class com.ciscowebex.androidsdk.omniusmodels.**{ *; } -keep enum com.ciscowebex.androidsdk.utils.internal.NetTypes{ *; +} +-keep class com.ciscowebex.androidsdk.phone.internal.RenderSink{ +*; } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/teams/membership/TeamFragmentTest.kt b/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/teams/membership/TeamFragmentTest.kt index b7dbea5..f8ce36a 100644 --- a/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/teams/membership/TeamFragmentTest.kt +++ b/app/src/androidTest/java/com/ciscowebex/androidsdk/kitchensink/teams/membership/TeamFragmentTest.kt @@ -74,13 +74,13 @@ class TeamFragmentTest : KitchenSinkTest() { onView(withId(R.id.teamsRecyclerView)).check(matches(hasDescendant(withText(testTeam)))) } -// @Test -// fun addPersonToTeam_teamFragment(){ -// goToMessagingActivity() -// onView(withId(R.id.teamsRecyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition(0, clickChildViewWithId(R.id.iv_add_to_team))) -// WaitUtils.sleep(TIME_1_SEC) -// intended(hasComponent(MessagingSearchActivity::class.java.name)) -// } + @Test + fun addPersonToTeam_teamFragment(){ + goToMessagingActivity() + onView(withId(R.id.teamsRecyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition(0, clickChildViewWithId(R.id.iv_add_to_team))) + WaitUtils.sleep(TIME_1_SEC) + intended(hasComponent(MessagingSearchActivity::class.java.name)) + } @Test fun testBottomSheetOptions_teamsFragment(){ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89033b6..2aa119d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -201,10 +201,15 @@ + + + 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 a32710f..af6c957 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt @@ -164,7 +164,9 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate, WebexAuthDelegat var _startAssociationLiveData: MutableLiveData? = null var _startShareLiveData: MutableLiveData? = null var _stopShareLiveData: MutableLiveData? = null - + var _startAudioDumpLiveData: MutableLiveData? = null + var _stopAudioDumpLiveData: MutableLiveData? = null + var _canStartAudioDumpLiveData: MutableLiveData? = null var _spaceEventLiveData: MutableLiveData>? = null var spaceEventListener : SpaceEventListener? = null var _membershipEventLiveData: MutableLiveData>? = null @@ -196,6 +198,9 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate, WebexAuthDelegat _startAssociationLiveData = null _startShareLiveData = null _stopShareLiveData = null + _startAudioDumpLiveData = null + _stopAudioDumpLiveData = null + _canStartAudioDumpLiveData = null } fun clearSpaceData(){ 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 3f3b94c..876bdd5 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt @@ -74,6 +74,9 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi val _setCompositeLayoutLiveData = MutableLiveData>() val _setRemoteVideoRenderModeLiveData = MutableLiveData>() val _forceSendingVideoLandscapeLiveData = MutableLiveData() + val _startAudioDumpLiveData = MutableLiveData() + val _stopAudioDumpLiveData = MutableLiveData() + val _canStartAudioDumpLiveData = MutableLiveData() var callMembershipsLiveData: LiveData> = _callMembershipsLiveData val muteAllLiveData: LiveData = _muteAllLiveData @@ -86,6 +89,9 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi val setCompositeLayoutLiveData: LiveData> = _setCompositeLayoutLiveData val setRemoteVideoRenderModeLiveData: LiveData> = _setRemoteVideoRenderModeLiveData val forceSendingVideoLandscapeLiveData: LiveData = _forceSendingVideoLandscapeLiveData + val startAudioDumpLiveData: LiveData = _startAudioDumpLiveData + val stopAudioDumpLiveData: LiveData = _stopAudioDumpLiveData + val canStartAudioDumpLiveData: LiveData = _canStartAudioDumpLiveData private val _incomingListenerLiveData = MutableLiveData() val incomingListenerLiveData: LiveData = _incomingListenerLiveData @@ -279,6 +285,9 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi repository._startAssociationLiveData = _startAssociationLiveData repository._startShareLiveData = _startShareLiveData repository._stopShareLiveData = _stopShareLiveData + repository._startAudioDumpLiveData = _startAudioDumpLiveData + repository._stopAudioDumpLiveData = _stopAudioDumpLiveData + repository._canStartAudioDumpLiveData = _canStartAudioDumpLiveData } fun setLogLevel(logLevel: String) { @@ -1458,4 +1467,42 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi repository.printObservers(writer) } + fun startAudioDump() { + getCall(currentCallId.orEmpty())?.startAudioDump(KitchenSinkApp.applicationContext()) { + if (it.isSuccessful) { + Log.d(tag, "[AudioDump] startAudioDump successful") + } else { + Log.d(tag, "[AudioDump] startAudioDump error: ${it.error?.errorMessage}") + } + _startAudioDumpLiveData.postValue(it.isSuccessful) + } + } + + fun stopAudioDump() { + getCall(currentCallId.orEmpty())?.stopAudioDump() { + if (it.isSuccessful) { + Log.d(tag, "[AudioDump] stopAudioDump successful") + } else { + Log.d(tag, "[AudioDump] stopAudioDump error: ${it.error?.errorMessage}") + } + _stopAudioDumpLiveData.postValue(it.isSuccessful) + } + + } + + fun canStartRecordingAudioDump() { + getCall(currentCallId.orEmpty())?.canStartRecordingAudioDump { + if (it.isSuccessful) { + Log.d(tag, "[AudioDump] canStartRecordingAudioDump successful") + } else { + Log.d(tag, "[AudioDump] canStartRecordingAudioDump error: ${it.error?.errorMessage}") + } + _canStartAudioDumpLiveData.postValue(it.isSuccessful) + } + } + + + fun isRecordingAudioDump(): Boolean { + return getCall(currentCallId.orEmpty())?.isRecordingAudioDump() ?: false + } } \ No newline at end of file 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 cc94a82..bcb59d7 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 @@ -28,7 +28,8 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni val sendDTMFClickListener: (Call?) -> Unit, val claimHostClickListener: () -> Unit, val showBreakoutSessions: () -> Unit, - val closedCaptionOptions: (Call?) -> Unit): BottomSheetDialogFragment() { + val closedCaptionOptions: (Call?) -> Unit, + val startAudioDumpListener: () -> Unit): BottomSheetDialogFragment() { companion object { val TAG = "CallBottomSheetFragment" } @@ -227,6 +228,11 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni closedCaptionOptions(call) } + startAudioDump.setOnClickListener { + dismiss() + startAudioDumpListener() + } + 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 2349501..d95f3d9 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 @@ -22,6 +22,7 @@ import android.os.Looper import android.util.Log import android.util.Pair import android.util.Rational +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE @@ -92,11 +93,14 @@ import com.ciscowebex.androidsdk.phone.Breakout import com.ciscowebex.androidsdk.phone.BreakoutSession import com.ciscowebex.androidsdk.phone.CompanionMode import com.ciscowebex.androidsdk.phone.ReceivingNoiseInfo +import com.ciscowebex.androidsdk.phone.RemoteShareCallback import com.ciscowebex.androidsdk.phone.ShareConfig import com.ciscowebex.androidsdk.phone.annotation.LiveAnnotationsPolicy import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo import com.ciscowebex.androidsdk.kitchensink.utils.GlobalExceptionHandler +import com.ciscowebex.androidsdk.kitchensink.CallManagementService +import com.ciscowebex.androidsdk.phone.AudioDumpResult import org.koin.android.ext.android.inject import com.ciscowebex.androidsdk.utils.internal.MimeUtils import com.google.android.material.snackbar.Snackbar @@ -107,7 +111,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel -class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface { +class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface, RemoteShareCallback { private val TAG = "CallControlsFragment" val webexViewModel: WebexViewModel by viewModel() val captionsViewModel: ClosedCaptionsViewModel by viewModel() @@ -151,6 +155,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface // Call onDisconnected is fired when user is the last one to leave main session and tries to join a breakout session. private var attemptingToJoinABreakoutSession = false private val mHandler = Handler(Looper.getMainLooper()) + private var callManagementServiceIntent: Intent? = null interface OnCallActionListener { fun onEndAndAnswer(currentCallId: String, newCallId: String, handler: CompletionHandler) @@ -211,13 +216,17 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface setUpViews(getArguments()) observerCallLiveData() initAudioManager() - // Enable below line to check is USM is enabled // Toast.makeText(requireActivity().applicationContext, "isUSMEnabled ${webexViewModel.webex.phone.isUnifiedSpaceMeetingEnabled()}", Toast.LENGTH_LONG).show() }.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.screenShareView.setRemoteShareCallback(this) + } + private fun initAudioManager() { audioManagerUtils = AudioManagerUtils(requireContext()) } @@ -285,8 +294,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface "isSelfCreator: ${webexViewModel.isSelfCreator()}, " + "isSpaceMeeting: ${webexViewModel.isSpaceMeeting()}, "+ "isScheduledMeeting: ${webexViewModel.isScheduledMeeting()}") - // Setting exception handler before making any call. Ideally this would be set in onCallConnected event - // but due to codegen limitation, we are setting it here. + // Setting exception handler before making any call. Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionHandler()) onCallConnected(call?.getCallId().orEmpty(), call?.isCUCMCall() ?: false, call?.isWebexCallingOrWebexForBroadworks() ?: false) webexViewModel.sendFeedback(call?.getCallId().orEmpty(), 5, "Testing Comments SDK-v3") @@ -310,7 +318,9 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } override fun onStartRinging(call: Call?, ringerType: Call.RingerType) { - Log.d(tag, "startRinger: $ringerType") + Log.d(TAG, "startRinger: $ringerType") + // Start call monitoring service when dialing a call + startCallMonitoringForegroundService() ringerManager.startRinger(ringerType) } @@ -326,6 +336,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface override fun onDisconnected(call: Call?, event: CallObserver.CallDisconnectedEvent?) { Log.d(TAG, "CallObserver onDisconnected : " + call?.getCallId()) Thread.setDefaultUncaughtExceptionHandler(null) + callManagementServiceIntent?.let { + activity?.stopService(it) + callManagementServiceIntent = null + } var callFailed = false var callEnded = false var localClose = false @@ -1059,6 +1073,46 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface onSignedOut() } }) + + webexViewModel.startAudioDumpLiveData.observe(viewLifecycleOwner, Observer { + it?.let { + if (it) { + Log.d(TAG, "startAudioDumpLiveData success") + showRecordingAlertDialog(requireActivity()) + } else { + Log.d(TAG, "startAudioDumpLiveData Failed") + showSnackbar("Failed to start audio dump") + } + } + }) + webexViewModel.stopAudioDumpLiveData.observe(viewLifecycleOwner, Observer { + it?.let { + if(alertDialogBuilder?.isShowing == true) { + alertDialogBuilder?.dismiss() + } + timerHandler.removeCallbacksAndMessages(null) + if (it) { + Log.d(TAG, "stopAudioDumpLiveData success") + } else { + Log.d(TAG, "stopAudioDumpLiveData Failed") + showSnackbar("Failed to stop audio dump") + } + } + }) + webexViewModel.canStartAudioDumpLiveData.observe(viewLifecycleOwner, Observer { + it?.let { + if (it) { + Log.d(TAG, "canStartAudioDumpLiveData success") + if(!webexViewModel.isRecordingAudioDump()) { + webexViewModel.startAudioDump() + } + + } else { + Log.d(TAG, "canStartAudioDumpLiveData Failed") + showSnackbar("Audio dump is not supported") + } + } + }) } private fun onSignedOut() { @@ -1452,7 +1506,8 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface { call -> sendDTMFClickListener(call) }, { claimHostClickListener() }, { showBreakoutSessions() }, - { call -> showCaptionDialog(call) }) + { call -> showCaptionDialog(call) }, + { startAudioDump() }) multiStreamOptionsBottomSheetFragment = MultiStreamOptionsBottomSheetFragment({ call -> setCategoryAOptionClickListener(call) }, { call -> setCategoryBOptionClickListener(call) }, @@ -1584,6 +1639,48 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface binding.annotationPolicy.setOnClickListener(this) } + private fun startAudioDump() { + webexViewModel.canStartRecordingAudioDump() + } + + var alertDialogBuilder : AlertDialog? = null + var timerHandler = Handler(Looper.getMainLooper()) + private fun showRecordingAlertDialog(activity: Activity) { + val builder = AlertDialog.Builder(requireActivity()) + builder.setTitle("Recording") + val timerTextView = TextView(activity).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + gravity = Gravity.CENTER_HORIZONTAL + textSize = 20f + } + + val startTime = System.currentTimeMillis() + val timerRunnable = object : Runnable { + override fun run() { + val millis = System.currentTimeMillis() - startTime + val seconds = (millis / 1000).toInt() + val minutes = seconds / 60 + val hours = minutes / 60 + timerTextView.text = String.format("%02d:%02d:%02d", hours, minutes % 60, seconds % 60) + timerHandler.postDelayed(this, 500) + } + } + timerHandler.postDelayed(timerRunnable, 0) + + builder.setView(timerTextView) + builder.setMessage("Audio recording is in progress:") + builder.setCancelable(false) + builder.setPositiveButton("STOP") { dialog, _ -> + dialog.dismiss() + timerHandler.removeCallbacks(timerRunnable) + webexViewModel.stopAudioDump() + } + + alertDialogBuilder = builder.create() + + alertDialogBuilder?.show() + } + private fun showCaptionDialog(call: Call?) { captionsController.showCaptionDialog(requireContext(), call) {intent, code -> startActivityForResult(intent, code) @@ -2322,13 +2419,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface // remoteStartedSharing : true means remote has started sharing else false means remote has ended sharing private fun onScreenShareVideoStreamInUseChanged(callId: String, remoteStartedSharing: Boolean? = null) { Log.d(TAG, "CallControlsFragment onScreenShareVideoStreamInUseChanged callerId: $callId") - if (webexViewModel.currentCallId != callId) { return } mHandler.post { - val remoteSharing = isReceivingSharing(callId) val localSharing = isLocalSharing(callId) Log.d(TAG, "CallControlsFragment onScreenShareVideoStreamInUseChanged isRemoteSharing: ${remoteSharing}, isLocalSharing: ${localSharing}") @@ -2506,6 +2601,8 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface private fun onIncomingCall(call: Call?, showBottomSheet : Boolean = true) { mHandler.post { Log.d(TAG, "CallControlsFragment onIncomingCall callerId: ${call?.getCallId()}, callInfo title: ${call?.getTitle()}") + // Start call monitoring service when incoming call is received. + startCallMonitoringForegroundService() binding.incomingCallHeader.visibility = View.GONE val schedules= call?.getSchedules() incomingLayoutState(false) @@ -3263,4 +3360,25 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface private fun showSnackbar(message: String) { Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() } + + // Remote share callback + override fun onShareStarted() { + Log.d(TAG, "onShareStarted") + } + + override fun onShareStopped() { + Log.d(TAG, "onShareStopped") + } + + override fun onFrameSizeChanged(width: Int, height: Int) { + Log.d(TAG, "onFrameSizeChanged width: $width, height: $height") + } + + private fun startCallMonitoringForegroundService() { + // Start CallManagement service to cut call when app is killed from recent tasks + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + callManagementServiceIntent = Intent(activity, CallManagementService::class.java) + activity?.startForegroundService(callManagementServiceIntent) + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_call_options.xml b/app/src/main/res/layout/bottom_sheet_call_options.xml index 57604ff..68883c4 100644 --- a/app/src/main/res/layout/bottom_sheet_call_options.xml +++ b/app/src/main/res/layout/bottom_sheet_call_options.xml @@ -382,6 +382,28 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/closedCaptionOptions" /> + + + + + app:layout_constraintTop_toBottomOf="@id/audioDumpSeparator" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_call_controls.xml b/app/src/main/res/layout/fragment_call_controls.xml index 7ddf0a4..7e79333 100644 --- a/app/src/main/res/layout/fragment_call_controls.xml +++ b/app/src/main/res/layout/fragment_call_controls.xml @@ -74,7 +74,7 @@ - Force Register Phone Services This will log you out of phone services from your other android devices and force register on this device Kitchen Sink running in background + Kitchen Sink call management service is running Keep App running in Background Fetching Details ... Caller details not found @@ -448,6 +449,7 @@ Enable Audio: None Closed Caption Options + Start Audio Dump Active From 91aba42908f57efcb7ced125fc7b230b1a2d8b23 Mon Sep 17 00:00:00 2001 From: Namratha A Date: Tue, 20 Aug 2024 23:15:38 +0530 Subject: [PATCH 2/3] 3.13 release update --- app/build.gradle | 5 ++--- .../com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt | 5 ++--- .../androidsdk/kitchensink/calling/CallControlsFragment.kt | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1ceacd6..356ba7c 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 3120000 - versionName "3.12.0" + versionCode 3130000 + versionName "3.13.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -124,7 +124,6 @@ dependencies { implementation 'com.ciscowebex:webexsdk:3.13.0' // For full flavor //implementation 'com.ciscowebex:webexsdk-wxc:3.13.0' //For webexCalling flavor //implementation 'com.ciscowebex:webexsdk-meeting:3.13.0' // For meeting flavor - implementation fileTree(dir: "libs", include: ["*.jar"]) implementation Dependencies.kotlinStdLib implementation Dependencies.coreKtx 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 876bdd5..3471a8b 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt @@ -45,7 +45,6 @@ import com.ciscowebex.androidsdk.phone.MediaStream import com.ciscowebex.androidsdk.phone.MediaStreamQuality import com.ciscowebex.androidsdk.phone.BreakoutSession import com.ciscowebex.androidsdk.phone.Breakout -import com.ciscowebex.androidsdk.phone.CompanionMode import com.ciscowebex.androidsdk.phone.DirectTransferResult import com.ciscowebex.androidsdk.phone.InviteParticipantError import com.ciscowebex.androidsdk.phone.SwitchToAudioVideoCallResult @@ -1468,7 +1467,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi } fun startAudioDump() { - getCall(currentCallId.orEmpty())?.startAudioDump(KitchenSinkApp.applicationContext()) { + getCall(currentCallId.orEmpty())?.startRecordingAudioDump(KitchenSinkApp.applicationContext()) { if (it.isSuccessful) { Log.d(tag, "[AudioDump] startAudioDump successful") } else { @@ -1479,7 +1478,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi } fun stopAudioDump() { - getCall(currentCallId.orEmpty())?.stopAudioDump() { + getCall(currentCallId.orEmpty())?.stopRecordingAudioDump() { if (it.isSuccessful) { Log.d(tag, "[AudioDump] stopAudioDump successful") } else { 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 d95f3d9..dfd1081 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 @@ -100,7 +100,6 @@ import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo import com.ciscowebex.androidsdk.kitchensink.utils.GlobalExceptionHandler import com.ciscowebex.androidsdk.kitchensink.CallManagementService -import com.ciscowebex.androidsdk.phone.AudioDumpResult import org.koin.android.ext.android.inject import com.ciscowebex.androidsdk.utils.internal.MimeUtils import com.google.android.material.snackbar.Snackbar From 60826463aac35215a68eb6f754ffff0f39e58c2a Mon Sep 17 00:00:00 2001 From: Namratha A Date: Wed, 21 Aug 2024 13:15:49 +0530 Subject: [PATCH 3/3] 3.13 release update --- .../kitchensink/CallManagementService.kt | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/CallManagementService.kt diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/CallManagementService.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/CallManagementService.kt new file mode 100644 index 0000000..6b61859 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/CallManagementService.kt @@ -0,0 +1,129 @@ +package com.ciscowebex.androidsdk.kitchensink + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage +import com.ciscowebex.androidsdk.phone.Call +import com.ciscowebex.androidsdk.phone.CallObserver + + +class CallManagementService : Service() { + private val tag = "CallManagementService" + private var receiver: BroadcastReceiver? = null + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (isForegroundServiceSupported()) { + startForeground(1, getNotification()) + } + return START_STICKY + } + + override fun onTaskRemoved(rootIntent: Intent?) { + Log.d(tag, "onTaskRemoved called!") + super.onTaskRemoved(rootIntent) + val call = getOngoingCall() + if (call != null) { + endCall(call) + } else { + Log.d(tag, "No call found in connected state. Killing service!") + stopSelf() + } + } + + override fun onDestroy() { + super.onDestroy() + if (receiver != null) { + unregisterReceiver(receiver) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getNotification(): Notification { + // Create a notification for the foreground service + val channelId = "ks_02" + val notificationChannel = + NotificationChannel( + channelId, "Call Management Service Notifications", + NotificationManager.IMPORTANCE_MIN + ) + notificationChannel.enableLights(false) + notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET + val mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + mNotificationManager.createNotificationChannel(notificationChannel) + + val title = getString(R.string.call_management_service_notification_title) + return NotificationCompat.Builder( + this, + channelId + ).setSmallIcon(R.drawable.app_notification_icon) + .setWhen(0) + .setContentTitle(title) + .setOngoing(true) + .build() + } + + private fun getOngoingCall(): Call? { + var call: Call? = null + + for (i in 0 until CallObjectStorage.size()) { + val callObj = CallObjectStorage.getCallObjectFromIndex(i) + val status = callObj?.getStatus() + if (status == Call.CallStatus.CONNECTED || status == Call.CallStatus.RINGING + || status == Call.CallStatus.WAITING || status == Call.CallStatus.INITIATED) { + Log.d(tag, "Call with id = ${callObj.getCallId()} found in ${status.name} state. ") + call = callObj + break + } + } + return call + } + + private fun endCall(call: Call) { + Log.d(tag, "Ending call with id = ${call.getCallId()}") + call.setObserver(object : CallObserver { + override fun onDisconnected(event: CallObserver.CallDisconnectedEvent?) { + Log.d(tag, "Call disconnected fired, stopping callManagementService!") + stopSelf() + } + + }) + if (call.getDirection() == Call.Direction.INCOMING && call.getStatus() == Call.CallStatus.RINGING) { + call.reject { + if (it.isSuccessful) { + Log.d(tag, "Call rejected successfully. Waiting for call disconnected event") + } else { + Log.e(tag, "Call reject failed, reason : ${it.error?.errorMessage}") + stopSelf() + } + } + } else { + call.hangup { result -> + if (result.isSuccessful) { + Log.d(tag, "Call hung up success. Waiting for call disconnected event") + } else { + Log.e(tag, "Call hangup failed, reason : ${result.error?.errorMessage}") + stopSelf() + } + } + } + + } + + private fun isForegroundServiceSupported(): Boolean { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + } +} \ No newline at end of file