diff --git a/app/build.gradle b/app/build.gradle index 1579da9..efa7674 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 3100100 - versionName "3.10.1" + versionCode 3110000 + versionName "3.11.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -63,6 +63,11 @@ android { buildConfigField "String", "SCOPE", "${SCOPE}" } + packagingOptions { + jniLibs { + pickFirsts += ['lib/armeabi-v7a/libc++_shared.so', 'lib/arm64-v8a/libc++_shared.so', 'lib/x86/libc++_shared.so', 'lib/x86_64/libc++_shared.so'] + } + } buildTypes { release { minifyEnabled true @@ -114,10 +119,10 @@ android { dependencies { - //At a time only one WebexSDK should be used. - implementation 'com.ciscowebex:webexsdk:3.10.1' // For full flavor - //implementation 'com.ciscowebex:webexsdk-wxc:3.10.1' //For webexCalling flavor - //implementation 'com.ciscowebex:webexsdk-meeting:3.10.1' // For meeting flavor + //At a time only one WebexSDK should be used. + implementation 'com.ciscowebex:webexsdk:3.11.0' // For full flavor + //implementation 'com.ciscowebex:webexsdk-wxc:3.11.0' //For webexCalling flavor + //implementation 'com.ciscowebex:webexsdk-meeting:3.11.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 b88e534..b31fb08 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -117,4 +117,7 @@ -keep enum com.ciscowebex.androidsdk.utils.internal.NetTypes{ *; -} \ No newline at end of file +} +-keep class com.cisco.newb.** { + *; + } \ 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 5cc336b..6a4e5a8 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt @@ -106,7 +106,8 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { InCorrectPasswordWithCaptcha, InCorrectPasswordOrHostKey, InCorrectPasswordOrHostKeyWithCaptcha, - WrongApiCalled + WrongApiCalled, + CannotStartInstantMeeting } enum class CalendarMeetingEvent { 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 548f492..780ab1d 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt @@ -12,6 +12,7 @@ import com.ciscowebex.androidsdk.kitchensink.firebase.RegisterTokenService import com.ciscowebex.androidsdk.kitchensink.person.PersonModel import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.WebexError +import com.ciscowebex.androidsdk.annotation.renderer.LiveAnnotationRenderer import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task import com.google.firebase.messaging.FirebaseMessaging @@ -34,6 +35,7 @@ import com.ciscowebex.androidsdk.phone.CallMembership import com.ciscowebex.androidsdk.phone.Phone import com.ciscowebex.androidsdk.phone.CallAssociationType import com.ciscowebex.androidsdk.phone.AdvancedSetting +import com.ciscowebex.androidsdk.phone.MakeHostError import com.ciscowebex.androidsdk.phone.AuxStream import com.ciscowebex.androidsdk.phone.VirtualBackground import com.ciscowebex.androidsdk.phone.CameraExposureISO @@ -44,10 +46,14 @@ import com.ciscowebex.androidsdk.phone.MediaStreamQuality import com.ciscowebex.androidsdk.phone.BreakoutSession import com.ciscowebex.androidsdk.phone.Breakout import com.ciscowebex.androidsdk.phone.DirectTransferResult +import com.ciscowebex.androidsdk.phone.InviteParticipantError 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.ReclaimHostError +import com.ciscowebex.androidsdk.phone.annotation.LiveAnnotationListener +import com.ciscowebex.androidsdk.phone.annotation.LiveAnnotationsPolicy import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo import com.google.firebase.installations.FirebaseInstallations @@ -98,6 +104,14 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi private val _initialSpacesSyncCompletedLiveData = MutableLiveData() val initialSpacesSyncCompletedLiveData: LiveData = _initialSpacesSyncCompletedLiveData + private val _annotationEvent = MutableLiveData() + val annotationEvent: LiveData get() = _annotationEvent + sealed class AnnotationEvent { + data class PERMISSION_ASK(val personId: String) : AnnotationEvent() + data class PERMISSION_EXPIRED(val personId: String) : AnnotationEvent() + } + + var selfPersonId: String? = null var compositedLayoutState = MediaOption.CompositedVideoLayout.NOT_SUPPORTED @@ -403,6 +417,9 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi WebexError.ErrorCode.INVALID_PASSWORD_OR_HOST_KEY_WITH_CAPTCHA.code -> { _callingLiveData.postValue(WebexRepository.CallLiveData(WebexRepository.CallEvent.InCorrectPasswordOrHostKeyWithCaptcha, null, error.data as Phone.Captcha)) } + WebexError.ErrorCode.CANNOT_START_INSTANT_MEETING.code -> { + _callingLiveData.postValue(WebexRepository.CallLiveData(WebexRepository.CallEvent.CannotStartInstantMeeting, null, null, result.error?.errorMessage)) + } else -> { _callingLiveData.postValue(WebexRepository.CallLiveData(WebexRepository.CallEvent.DialFailed, null, null, result.error?.errorMessage)) } @@ -463,6 +480,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi override fun onDisconnected(event: CallObserver.CallDisconnectedEvent?) { Log.d(tag, "CallObserver onDisconnected event: ${this@WebexViewModel} $callObserverInterface $event") callObserverInterface?.onDisconnected(call, event) + annotationRenderer?.stopRendering() } override fun onInfoChanged(call: Call?) { @@ -621,15 +639,22 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi } fun startShare(callId: String, shareConfig: ShareConfig?) { - getCall(callId)?.startSharing(CompletionHandler { result -> - _startShareLiveData.postValue(result.isSuccessful) - }, shareConfig) + val call = getCall(callId) + call?.let { + it.startSharing(CompletionHandler { result -> + _startShareLiveData.postValue(result.isSuccessful) + + }, shareConfig) + } } fun startShare(callId: String, notification: Notification?, notificationId: Int, shareConfig: ShareConfig?) { - getCall(callId)?.startSharing(notification, notificationId, CompletionHandler { result -> - _startShareLiveData.postValue(result.isSuccessful) - }, shareConfig) + val call = getCall(callId) + call?.let { + it.startSharing(notification, notificationId, CompletionHandler { result -> + _startShareLiveData.postValue(result.isSuccessful) + }, shareConfig) + } } fun setSendingSharing(callId: String, value: Boolean) { @@ -642,6 +667,80 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi }) } + private var annotationRenderer: LiveAnnotationRenderer? = null + fun initalizeAnnotations(renderer: LiveAnnotationRenderer) { + getCall(currentCallId.orEmpty())?.getLiveAnnotationHandle()?.let {annotations-> + + annotations.setLiveAnnotationsPolicy(LiveAnnotationsPolicy.NeedAskForAnnotate){ + if (it.isSuccessful) { + Log.d(tag, "setLiveAnnotationsPolicy successful") + } else { + Log.d(tag, "setLiveAnnotationsPolicy error: ${it.error?.errorMessage}") + } + } + + annotations.setLiveAnnotationListener(object : LiveAnnotationListener { + override fun onLiveAnnotationRequestReceived(personId: String) { + _annotationEvent.postValue(AnnotationEvent.PERMISSION_ASK(personId)) + } + + override fun onLiveAnnotationRequestExpired(personId: String) { + _annotationEvent.postValue(AnnotationEvent.PERMISSION_EXPIRED(personId)) + } + + override fun onLiveAnnotationsStarted() { + annotationRenderer = renderer.apply { + setAnnotationRendererCallback(object : LiveAnnotationRenderer.LiveAnnotationRendererCallback { + override fun onAnnotationRenderingReady() { + Log.d(tag, "onAnnotationRenderingReady") + } + + override fun onAnnotationRenderingStopped() { + Log.d(tag, "onAnnotationRenderingStopped") + getCall(currentCallId.orEmpty())?.getLiveAnnotationHandle()?.stopLiveAnnotations() + annotationRenderer = null + } + }) + startRendering() + } + } + + override fun onLiveAnnotationDataArrived(data: String) { + annotationRenderer?.renderData(data) + } + + override fun onLiveAnnotationsStopped() { + annotationRenderer?.stopRendering() + } + + }) + } + } + + fun handleAnnotationPermission(grant: Boolean, personId: String) { + getCall(currentCallId.orEmpty())?.getLiveAnnotationHandle()?.respondToLiveAnnotationRequest(personId, grant) { + if (it.isSuccessful) { + Log.d(tag, "permission handled") + } else { + Log.d(tag, "permission error: ${it.error?.errorMessage}") + } + } + } + + fun getCurrentLiveAnnotationPolicy(): LiveAnnotationsPolicy? { + return getCall(currentCallId.orEmpty())?.getLiveAnnotationHandle()?.getLiveAnnotationsPolicy() + } + + fun setLiveAnnotationPolicy(policy: LiveAnnotationsPolicy) { + getCall(currentCallId.orEmpty())?.getLiveAnnotationHandle()?.setLiveAnnotationsPolicy(policy) { + if (it.isSuccessful) { + Log.d(tag, "setLiveAnnotationsPolicy successful") + } else { + Log.d(tag, "setLiveAnnotationsPolicy error: ${it.error?.errorMessage}") + } + } + } + fun sendFeedback(callId: String, rating: Int, comment: String) { getCall(callId)?.sendFeedback(rating, comment) } @@ -1268,6 +1367,30 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi return webex.phone.getCallingType() } + fun makeHost(participantId: String, handler: CompletionHandler) { + getCall(currentCallId.orEmpty())?.makeHost(participantId) { result -> + if (result.isSuccessful) { + Log.d(tag, "Make host successful") + handler.onComplete(ResultImpl.success()) + } else { + Log.d(tag, "Make host failed") + handler.onComplete(ResultImpl.error(result.error?.errorMessage)) + } + } + } + + fun reclaimHost(hostKey:String, handler: CompletionHandler) { + getCall(currentCallId.orEmpty())?.reclaimHost(hostKey) { result -> + if (result.isSuccessful) { + Log.d(tag, "reclaimHost successful") + handler.onComplete(ResultImpl.success()) + } else { + Log.d(tag, "reclaimHost failed") + handler.onComplete(ResultImpl.error(result.error?.errorMessage)) + } + } + } + fun setOnInitialSpacesSyncCompletedListener() { repository.setOnInitialSpacesSyncCompletedListener() { _initialSpacesSyncCompletedLiveData.postValue(true) @@ -1294,6 +1417,18 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi return getCall(currentCallId.orEmpty())?.isVideoEnabled() ?: false } + fun inviteParticipant(invitee: String, callback: CompletionHandler) { + getCall(currentCallId.orEmpty())?.inviteParticipant(invitee) { result -> + if (result.isSuccessful) { + Log.d(tag, "InviteParticipant successful") + callback.onComplete(ResultImpl.success()) + } else { + Log.d(tag, "InviteParticipant failed") + callback.onComplete(ResultImpl.error(result.error?.errorMessage)) + } + } + } + fun cleanup() { repository.removeIncomingCallListener("viewmodel"+this) for (entry in callObserverMap.entries.iterator()) { @@ -1317,4 +1452,5 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi writer.println("******************") repository.printObservers(writer) } + } \ 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 53ab424..cc94a82 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,6 +26,7 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni val cameraOptionsClickListener: (Call?) -> Unit, val multiStreamOptionsClickListener: (Call?) -> Unit, val sendDTMFClickListener: (Call?) -> Unit, + val claimHostClickListener: () -> Unit, val showBreakoutSessions: () -> Unit, val closedCaptionOptions: (Call?) -> Unit): BottomSheetDialogFragment() { companion object { @@ -183,6 +184,11 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni transcriptionClickListener(call) } + claimHost.setOnClickListener { + dismiss() + claimHostClickListener() + } + showIncomingCall.setOnClickListener { dismiss() showIncomingCallsClickListener(call) 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 28c16f1..722e3ac 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 @@ -24,7 +24,9 @@ import android.util.Pair import android.util.Rational import android.view.LayoutInflater import android.view.View +import android.view.View.INVISIBLE import android.view.View.OnClickListener +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView import android.widget.ImageButton @@ -43,6 +45,8 @@ import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.WebexError +import com.ciscowebex.androidsdk.annotation.renderer.LiveAnnotationRenderer +import com.ciscowebex.androidsdk.kitchensink.BuildConfig import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.WebexRepository import com.ciscowebex.androidsdk.kitchensink.WebexViewModel @@ -66,6 +70,7 @@ import com.ciscowebex.androidsdk.kitchensink.utils.extensions.toast import com.ciscowebex.androidsdk.kitchensink.utils.showDialogForDTMF import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage import com.ciscowebex.androidsdk.kitchensink.utils.UIUtils +import com.ciscowebex.androidsdk.kitchensink.utils.showDialogForHostKey import com.ciscowebex.androidsdk.message.LocalFile import com.ciscowebex.androidsdk.phone.AuxStream import com.ciscowebex.androidsdk.phone.Call @@ -86,6 +91,7 @@ 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.annotation.LiveAnnotationsPolicy import com.ciscowebex.androidsdk.phone.closedCaptions.CaptionItem import com.ciscowebex.androidsdk.phone.closedCaptions.ClosedCaptionsInfo import org.koin.android.ext.android.inject @@ -128,12 +134,13 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface var onCallActionListener: OnCallActionListener? = null private var breakoutSessions : List = emptyList() private var breakout: Breakout? = null - private var dialType = DialType.NONE; + private var dialType = DialType.NONE private val mediaPlayer: MediaPlayer = MediaPlayer() private lateinit var passwordDialogBinding: DialogEnterMeetingPinBinding private lateinit var passwordDialog : Dialog - private var isInPipMode = false; + private var isInPipMode = false private var screenShareOptionsDialog: AlertDialog? = null + private lateinit var annotationPermissionDialog: AlertDialog // Is true when trying to join a Breakout Session, and becomes false when successfully joined or error occurs // Call onDisconnected is fired when user is the last one to leave main session and tries to join a breakout session. @@ -497,6 +504,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface if (event.isAvailable()) { if (event.getStream()?.getStreamType() == MediaStreamType.Stream1) { //remote media stream + onVideoStreamingChanged(webexViewModel.currentCallId.toString()) setRemoteVideoInformation(event.getStream()?.getPerson()?.getDisplayName().orEmpty(), !(event.getStream()?.getPerson()?.isSendingAudio() ?: true)) } else { Log.d(TAG, "CallObserver OnMediaChanged MediaStreamAvailabilityEvent personID: ${event.getStream()?.getPerson()?.getPersonId()}," + @@ -832,6 +840,13 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface status?.let { if (it) { Log.d(TAG, "startShareLiveData success") + // For this share screen session initialize live annotations + webexViewModel.initalizeAnnotations(LiveAnnotationRenderer(requireContext())) + if(BuildConfig.FLAVOR != "wxc") { + binding.annotationPolicy.visibility = VISIBLE + binding.annotationPolicy.text = webexViewModel.getCurrentLiveAnnotationPolicy().toString() + } + } else { updateScreenShareButtonState(ShareButtonState.OFF) Log.d(TAG, "User cancelled screen request") @@ -847,6 +862,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface Log.d(TAG, "stopShareLiveData Failed") } } + binding.annotationPolicy.visibility = INVISIBLE }) webexViewModel.setCompositeLayoutLiveData.observe(viewLifecycleOwner, Observer { result -> @@ -885,10 +901,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface onCallJoined(call) handleCallControls(call) } - WebexRepository.CallEvent.DialFailed, WebexRepository.CallEvent.WrongApiCalled -> { + WebexRepository.CallEvent.DialFailed, WebexRepository.CallEvent.WrongApiCalled, WebexRepository.CallEvent.CannotStartInstantMeeting -> { dismissErrorDialog() val callActivity = activity as CallActivity? - callActivity?.alertDialog(true, errorMessage ?: "") + callActivity?.alertDialog(true, errorMessage ?: event.name) } WebexRepository.CallEvent.AnswerCompleted -> { webexViewModel.currentCallId = call?.getCallId() @@ -1017,6 +1033,13 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface bottomSheetFragment?.backgrounds?.add(emptyBackground) bottomSheetFragment?.adapter?.notifyDataSetChanged() }) + + webexViewModel.annotationEvent.observe(viewLifecycleOwner) { event -> + when (event) { + is WebexViewModel.AnnotationEvent.PERMISSION_ASK -> toggleAnnotationPermissionDialog(true, event.personId) + is WebexViewModel.AnnotationEvent.PERMISSION_EXPIRED -> toggleAnnotationPermissionDialog(false, event.personId) + } + } } private fun handleOnBackgroundChanged(virtualBackground: VirtualBackground) { @@ -1402,6 +1425,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface { call -> cameraOptionsClickListener(call) }, { call -> multiStreamOptionsClickListener(call) }, { call -> sendDTMFClickListener(call) }, + { claimHostClickListener() }, { showBreakoutSessions() }, { call -> showCaptionDialog(call) }) @@ -1526,6 +1550,8 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface binding.btnReturnToMainSession.visibility = View.INVISIBLE passwordDialog = Dialog(requireContext()) + + binding.annotationPolicy.setOnClickListener(this) } private fun showCaptionDialog(call: Call?) { @@ -1647,12 +1673,37 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface Log.d(TAG, "ReceivingNoiseRemoval enableAPIResult = error : ${it.error?.errorMessage.orEmpty()}") } } + binding.annotationPolicy -> { + showPolicySelectionListDialog() + } else -> { } } } } + private fun showPolicySelectionListDialog() { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(getString(R.string.annotation_policy)) + val policyList = { resources.getStringArray(R.array.annotation_policy) } + builder.setSingleChoiceItems(policyList(), 0) { dialog, which -> + when (which) { + 0 -> { + webexViewModel.setLiveAnnotationPolicy(LiveAnnotationsPolicy.NobodyCanAnnotate) + } + 1 -> { + webexViewModel.setLiveAnnotationPolicy(LiveAnnotationsPolicy.AnyoneCanAnnotate) + } + 2 -> { + webexViewModel.setLiveAnnotationPolicy(LiveAnnotationsPolicy.NeedAskForAnnotate) + } + } + binding.annotationPolicy.text = policyList()[which] + dialog.dismiss() + } + builder.show() + } + private fun mainContentLayoutClickListener() { Log.d(TAG, "mainContentLayoutClickListener") @@ -1820,6 +1871,26 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface screenShareOptionsDialog?.show() } + private fun toggleAnnotationPermissionDialog(show: Boolean, personID: String?) { + if (show) { + // Show the permission dialog + annotationPermissionDialog = AlertDialog.Builder(context) + .setTitle("Live Annotation Permission") + .setMessage("Annotation request received.") + .setPositiveButton(getString(R.string.accept)) { _, _ -> + webexViewModel.handleAnnotationPermission(true, personID!!) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + webexViewModel.handleAnnotationPermission(false, personID!!) + } + .create() + + annotationPermissionDialog.show() + } else { + if(annotationPermissionDialog.isShowing) annotationPermissionDialog.dismiss() + } + } + fun needBackPressed(): Boolean { if (isIncomingActivity && webexViewModel.currentCallId == null) { @@ -2783,6 +2854,24 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface }) } + private fun claimHostClickListener() { + Log.d(TAG, "claimHostClickListener") + showDialogForHostKey(requireContext(), getString(R.string.enter_host_key), onPositiveButtonClick = { dialog: DialogInterface, number: String -> + webexViewModel.reclaimHost(number){ + if (it.isSuccessful) { + showToast("Reclaim Host Successful") + Log.d(TAG, "Reclaim Host Successful") + } else { + showToast("Reclaim Host failed ${it.error?.errorMessage}") + Log.d(TAG, "Reclaim Host failed ${it.error?.errorMessage}") + } + } + dialog.dismiss() + }, onNegativeButtonClick = { dialog: DialogInterface, _: Int -> + dialog.dismiss() + }) + } + private fun setCategoryAOptionClickListener(call: Call?) { Log.d(TAG, "setCategoryAOptionClickListener") showMultiStreamDataOptionsBottomSheetFragment(call, MultiStreamDataOptionsBottomSheetFragment.OptionType.CategoryA) @@ -3140,4 +3229,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface mediaPlayer.reset() super.onStop() } + private fun showToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsAdapter.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsAdapter.kt index ead0cf5..2f3495a 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsAdapter.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsAdapter.kt @@ -3,14 +3,13 @@ package com.ciscowebex.androidsdk.kitchensink.calling.participants import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.databinding.ParticipantsHeaderItemBinding import com.ciscowebex.androidsdk.kitchensink.databinding.ParticipantsListItemBinding import com.ciscowebex.androidsdk.phone.CallMembership -class ParticipantsAdapter(private val participants: ArrayList, private val itemClickListener: OnItemActionListener, private val selfId: String) : RecyclerView.Adapter() { +class ParticipantsAdapter(private val participants: ArrayList, private val itemClickListener: OnItemActionListener, private val selfId: String, private val isSelfModerator: Boolean) : RecyclerView.Adapter() { private val viewTypeHeader = 0 private val viewTypeParticipant = 1 @@ -60,6 +59,10 @@ class ParticipantsAdapter(private val participants: ArrayList, private val binding.imgMute.setImageResource(R.drawable.ic_mic_off_24) binding.imgMute.visibility = if(!participant.isSendingAudio()) View.VISIBLE else View.INVISIBLE binding.infoDeviceType.text = participant.getDeviceType().name + binding.presenter.visibility = if(participant.isPresenter()) View.VISIBLE else View.GONE + binding.host.visibility = if(participant.isHost()) View.VISIBLE else View.GONE + binding.cohost.visibility = if(participant.isCohost()) View.VISIBLE else View.GONE + binding.makeHost.visibility = if(!participant.isSelf() && isSelfModerator) View.VISIBLE else View.GONE val personId = participant.getPersonId() @@ -77,7 +80,9 @@ class ParticipantsAdapter(private val participants: ArrayList, private val itemClickListener.onLetInClicked(participant) true } - + binding.makeHost.setOnClickListener { + itemClickListener.onMakeHostClicked(participant.getPersonId()) + } } } @@ -91,6 +96,9 @@ class ParticipantsAdapter(private val participants: ArrayList, private val interface OnItemActionListener{ fun onParticipantMuted(participantId: String, hasPairedParticipant: Boolean) + fun onLetInClicked(callMembership: CallMembership) + + fun onMakeHostClicked(participantId: String) } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsFragment.kt index 2f69b7b..b1039b4 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsFragment.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/participants/ParticipantsFragment.kt @@ -17,7 +17,10 @@ import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.WebexViewModel import com.ciscowebex.androidsdk.kitchensink.calling.CallActivity import com.ciscowebex.androidsdk.kitchensink.databinding.FragmentParticipantsBinding +import com.ciscowebex.androidsdk.kitchensink.utils.showDialogForTextBox import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage +import com.ciscowebex.androidsdk.phone.MakeHostError +import com.ciscowebex.androidsdk.phone.InviteParticipantError import com.ciscowebex.androidsdk.phone.CallMembership import kotlinx.android.synthetic.main.fragment_participants.* @@ -29,6 +32,7 @@ class ParticipantsFragment : DialogFragment(), ParticipantsAdapter.OnItemActionL private lateinit var webexViewModel: WebexViewModel private var currentCallId: String? = null private var selfId: String? = null + private var isSelfModerator: Boolean = false companion object { private const val CALL_KEY = "call_id" @@ -64,9 +68,17 @@ class ParticipantsFragment : DialogFragment(), ParticipantsAdapter.OnItemActionL } private fun setUpViews() { - adapter = ParticipantsAdapter(arrayListOf(), this, selfId.orEmpty()) + val callMembership = webexViewModel.getCall(webexViewModel.currentCallId.orEmpty())?.getMemberships() + for(member in callMembership.orEmpty()) + { + if(member.isSelf() && member.isHost()) + { + isSelfModerator = true + break + } + } + adapter = ParticipantsAdapter(arrayListOf(), this, selfId.orEmpty(), isSelfModerator) binding.participants.adapter = adapter - val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL) binding.participants.addItemDecoration(dividerItemDecoration) @@ -76,7 +88,7 @@ class ParticipantsFragment : DialogFragment(), ParticipantsAdapter.OnItemActionL webexViewModel.getParticipants(_callId) } - webexViewModel.callMembershipsLiveData.observe(this, Observer { + webexViewModel.callMembershipsLiveData.observe(this, Observer { it -> it?.let { callMemberships -> Log.d(tag, callMemberships.toString()) val data = arrayListOf() @@ -107,6 +119,9 @@ class ParticipantsFragment : DialogFragment(), ParticipantsAdapter.OnItemActionL showToast(getString(R.string.mute_feature_is_not_available_for_cucm_calls)) } } + binding.inviteParticipantButton.setOnClickListener { + inviteParticipant() + } binding.close.setOnClickListener { dismiss() } @@ -146,4 +161,44 @@ class ParticipantsFragment : DialogFragment(), ParticipantsAdapter.OnItemActionL } } } + + private fun inviteParticipant() { + showDialogForTextBox(requireContext(), getString(R.string.invite_participant), onPositiveButtonClick = { dialog: DialogInterface, invitee: String -> + webexViewModel.inviteParticipant(invitee) { + if (it.isSuccessful) { + showToast("Invite Participant Successful") + Log.d(tag, "Invite Participant Successful") + } else { + showToast("Invite Participant failed ${it.error?.errorMessage}") + Log.d(tag, "Invite Participant failed ${it.error?.errorMessage}") + } + } + dialog.dismiss() + }, onNegativeButtonClick = { dialog: DialogInterface, _: Int -> + dialog.dismiss() + }) + } + + override fun onMakeHostClicked(participantId: String) { + context?.let { + ctx-> + showDialogWithMessage(ctx, getString(R.string.message), getString(R.string.assign_host_confirmation), + onPositiveButtonClick = { dialog, _ -> + currentCallId?.let { + webexViewModel.makeHost(participantId) { + if (it.isSuccessful) { + showToast(getString(R.string.assign_host_success)) + } else { + showToast(it.error?.errorMessage ?: getString(R.string.assign_host_failure)) + } + + } + } + dialog.dismiss() + }, + onNegativeButtonClick = { dialog, _ -> + dialog.dismiss() + }) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchCommonFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchCommonFragment.kt index 732645f..ca30b13 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchCommonFragment.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchCommonFragment.kt @@ -135,7 +135,12 @@ class SearchCommonFragment : Fragment() { if (item == null) { val itemModel = ItemModel() itemModel.name = it[i].title - itemModel.image = R.drawable.ic_call + if(it[i].spaceType == Space.SpaceType.DIRECT) { + itemModel.image = R.drawable.ic_call + } + else { + itemModel.image = 0 + } itemModel.callerId = id itemModel.ongoing = searchViewModel.isSpaceCallStarted() && searchViewModel.spaceCallId() == id itemModel.isExternallyOwned = it[i].isExternallyOwned ?: false @@ -317,6 +322,10 @@ class SearchCommonFragment : Fragment() { fun bind(itemModel: ItemModel) { binding.listItem = itemModel + if(itemModel.image == 0) + binding.image.visibility = View.GONE + else + binding.image.visibility = View.VISIBLE binding.image.setOnClickListener { it.context.startActivity(CallActivity.getOutgoingIntent(it.context, itemModel.callerId, itemModel.isPhoneNumber)) } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DialogUtils.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DialogUtils.kt index 2cffcb0..07bc378 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DialogUtils.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DialogUtils.kt @@ -64,3 +64,33 @@ fun showDialogForDTMF(context: Context, title: String, positiveButtonText: Int = .show() } +fun showDialogForHostKey(context: Context, title: String, positiveButtonText: Int = android.R.string.ok, + onPositiveButtonClick: (DialogInterface, String) -> Unit, negativeButtonText: Int = android.R.string.cancel, + onNegativeButtonClick: (DialogInterface, Int) -> Unit) { + val input = EditText(context) + AlertDialog.Builder(context) + .setTitle(title) + .setView(input) + .setPositiveButton(positiveButtonText) { dialogInterface: DialogInterface, i: Int -> + val hostKey = input.text.toString() + onPositiveButtonClick(dialogInterface, hostKey) + } + .setNegativeButton(negativeButtonText, onNegativeButtonClick) + .show() +} + +fun showDialogForTextBox(context: Context, title: String, positiveButtonText: Int = android.R.string.ok, + onPositiveButtonClick: (DialogInterface, String) -> Unit, negativeButtonText: Int = android.R.string.cancel, + onNegativeButtonClick: (DialogInterface, Int) -> Unit) { + val input = EditText(context) + AlertDialog.Builder(context) + .setTitle(title) + .setView(input) + .setPositiveButton(positiveButtonText) { dialogInterface: DialogInterface, i: Int -> + val text = input.text.toString() + onPositiveButtonClick(dialogInterface, text) + } + .setNegativeButton(negativeButtonText, onNegativeButtonClick) + .show() +} + 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 de36d67..57604ff 100644 --- a/app/src/main/res/layout/bottom_sheet_call_options.xml +++ b/app/src/main/res/layout/bottom_sheet_call_options.xml @@ -73,6 +73,28 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/showTranscripts" /> + + + + + app:layout_constraintTop_toBottomOf="@id/claimHostSeparator" /> + app:layout_constraintTop_toTopOf="@id/iv_cancel_call" + tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/fragment_participants.xml b/app/src/main/res/layout/fragment_participants.xml index c9fbbb7..8932aef 100644 --- a/app/src/main/res/layout/fragment_participants.xml +++ b/app/src/main/res/layout/fragment_participants.xml @@ -36,6 +36,19 @@ app:layout_constraintTop_toTopOf="parent" android:text="@string/participants" /> + + + + + + + + + +