From 32ddcd66ef6f07c8a726106ce4542dc3e811cd6d Mon Sep 17 00:00:00 2001 From: Raghavendran Sundaraganesh Date: Wed, 24 Aug 2022 12:13:14 +0530 Subject: [PATCH] 3.6.0 Release --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 28 +- .../androidsdk/kitchensink/HomeActivity.kt | 9 +- .../androidsdk/kitchensink/KitchenSinkApp.kt | 21 + .../KitchenSinkForegroundService.kt | 108 ++++ .../androidsdk/kitchensink/WebexRepository.kt | 25 +- .../androidsdk/kitchensink/WebexViewModel.kt | 47 +- .../kitchensink/calling/CallActivity.kt | 3 - .../calling/CallBottomSheetFragment.kt | 7 +- .../calling/CallControlsFragment.kt | 281 ++++++++-- .../kitchensink/calling/CucmCallActivity.kt | 186 +++++++ .../calling/MediaStreamBottomSheetFragment.kt | 99 ++++ .../kitchensink/cucm/UCLoginActivity.kt | 164 ++++-- .../firebase/KitchenSinkFCMService.kt | 95 +++- .../composer/MessageComposerActivity.kt | 3 +- .../messaging/spaces/SpacesFragment.kt | 5 +- .../messaging/spaces/SpacesRepository.kt | 4 +- .../messaging/spaces/SpacesViewModel.kt | 24 +- .../MessageActionBottomSheetFragment.kt | 8 +- .../spaces/detail/SpaceDetailActivity.kt | 7 +- .../spaces/detail/SpaceDetailViewModel.kt | 4 +- .../search/SearchCommonFragment.kt | 81 ++- .../kitchensink/search/SearchRepository.kt | 15 +- .../kitchensink/search/SearchViewModel.kt | 8 +- .../kitchensink/setup/SetupActivity.kt | 13 + .../androidsdk/kitchensink/utils/Constants.kt | 3 + .../androidsdk/kitchensink/utils/DateUtils.kt | 12 + .../kitchensink/utils/SharedPrefUtils.kt | 15 + .../main/res/drawable/border_category_a.xml | 7 + .../main/res/drawable/border_category_b.xml | 7 + .../main/res/drawable/border_category_c.xml | 7 + .../main/res/drawable/ic_call_incoming.xml | 10 + .../main/res/drawable/ic_call_outgoing.xml | 10 + app/src/main/res/drawable/ic_more_black.xml | 10 + app/src/main/res/drawable/ic_pin.xml | 10 + .../main/res/layout/activity_cucm_call.xml | 86 +++ .../main/res/layout/activity_cucm_login.xml | 6 +- app/src/main/res/layout/activity_setup.xml | 525 ++++++++++-------- .../res/layout/bottom_sheet_media_stream.xml | 148 +++++ .../layout/bottom_sheet_message_options.xml | 24 +- .../res/layout/common_fragment_item_list.xml | 33 +- .../res/layout/fragment_call_controls.xml | 2 + app/src/main/res/layout/multistream_view.xml | 33 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 20 +- 45 files changed, 1761 insertions(+), 459 deletions(-) create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkForegroundService.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CucmCallActivity.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/MediaStreamBottomSheetFragment.kt create mode 100644 app/src/main/res/drawable/border_category_a.xml create mode 100644 app/src/main/res/drawable/border_category_b.xml create mode 100644 app/src/main/res/drawable/border_category_c.xml create mode 100644 app/src/main/res/drawable/ic_call_incoming.xml create mode 100644 app/src/main/res/drawable/ic_call_outgoing.xml create mode 100644 app/src/main/res/drawable/ic_more_black.xml create mode 100644 app/src/main/res/drawable/ic_pin.xml create mode 100644 app/src/main/res/layout/activity_cucm_call.xml create mode 100644 app/src/main/res/layout/bottom_sheet_media_stream.xml diff --git a/app/build.gradle b/app/build.gradle index b4d4339..4173867 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId "com.cisco.sdk_android" minSdkVersion Versions.minSdk targetSdkVersion Versions.targetSdk - versionCode 35000 - versionName "3.5.0" + versionCode 36000 + versionName "3.6.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "CLIENT_ID", "${CLIENT_ID}" @@ -63,7 +63,7 @@ android { } dependencies { - implementation 'com.ciscowebex:androidsdk:3.5.0' + implementation 'com.ciscowebex:androidsdk:3.6.0' implementation fileTree(dir: "libs", include: ["*.jar"]) implementation Dependencies.kotlinStdLib diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be2dcf0..e9f31e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + @@ -63,13 +64,13 @@ + android:theme="@style/AppTheme"> + @@ -149,20 +154,21 @@ - - - + - + - - + \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt index fa28e3b..22fbdb1 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt @@ -48,6 +48,10 @@ class HomeActivity : BaseActivity() { webexViewModel.setLogLevel(webexViewModel.logFilter) webexViewModel.enableConsoleLogger(webexViewModel.isConsoleLoggerEnabled) + if(SharedPrefUtils.isAppBackgroundRunningPreferred(this)) { + KitchenSinkForegroundService.startForegroundService(this) + } + Log.d(tag, "Service URls METRICS: ${webexViewModel.getServiceUrl(Phone.ServiceUrlType.METRICS)}" + "\nCLIENT_LOGS: ${webexViewModel.getServiceUrl(Phone.ServiceUrlType.CLIENT_LOGS)}" + "\nKMS: ${webexViewModel.getServiceUrl(Phone.ServiceUrlType.KMS)}") @@ -72,6 +76,7 @@ class HomeActivity : BaseActivity() { if (it) { clearLoginTypePref(this) (application as KitchenSinkApp).unloadKoinModules() + KitchenSinkForegroundService.stopForegroundService(this) finish() } else { @@ -174,10 +179,6 @@ class HomeActivity : BaseActivity() { webexViewModel.setCalendarMeetingObserver() } - override fun onBackPressed() { - (application as KitchenSinkApp).closeApplication() - } - private fun showMessageIfCameFromNotification() { if("ACTION" == intent?.action){ diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt index b59ea04..b7da212 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt @@ -15,6 +15,7 @@ import com.ciscowebex.androidsdk.kitchensink.messaging.messagingModule import com.ciscowebex.androidsdk.kitchensink.messaging.search.searchPeopleModule import com.ciscowebex.androidsdk.kitchensink.person.personModule import com.ciscowebex.androidsdk.kitchensink.search.searchModule +import com.ciscowebex.androidsdk.kitchensink.utils.SharedPrefUtils import com.ciscowebex.androidsdk.kitchensink.webhooks.webhooksModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -38,6 +39,13 @@ class KitchenSinkApp : Application(), LifecycleObserver { } var inForeground: Boolean = false + + + // App level boolean to keep track of if the CUCM login is of type SSO Login + var isUCSSOLogin = false + + var isKoinModulesLoaded : Boolean = false + } override fun onCreate() { @@ -70,6 +78,17 @@ class KitchenSinkApp : Application(), LifecycleObserver { android.os.Process.killProcess(android.os.Process.myPid()) } + + fun loadModules(): Boolean { + val type = SharedPrefUtils.getLoginTypePref(this@KitchenSinkApp) + if(type != null) { + loadKoinModules(LoginActivity.LoginType.valueOf(type)) + return true + } + return false + } + + fun loadKoinModules(type: LoginActivity.LoginType) { when (type) { LoginActivity.LoginType.JWT -> { @@ -82,9 +101,11 @@ class KitchenSinkApp : Application(), LifecycleObserver { loadKoinModules(listOf(mainAppModule, webexModule, loginModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) } } + isKoinModulesLoaded = true } fun unloadKoinModules() { unloadKoinModules(listOf(mainAppModule, webexModule, loginModule, JWTWebexModule, AccessTokenWebexModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) + isKoinModulesLoaded = false } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkForegroundService.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkForegroundService.kt new file mode 100644 index 0000000..c8d96a4 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkForegroundService.kt @@ -0,0 +1,108 @@ +package com.ciscowebex.androidsdk.kitchensink + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.app.PendingIntent +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 androidx.core.content.ContextCompat +import com.ciscowebex.androidsdk.kitchensink.auth.LoginActivity + +class KitchenSinkForegroundService : Service() { + + private var mNotificationManager: NotificationManager? = null; + + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onCreate() { + super.onCreate() + mNotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (IsForegroundServiceSupported()) { + startForeground(0x111111, getServiceOngoingNotification(this)) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.let { + if (it.getBooleanExtra(KitchenSinkForegroundService.STOP_REQUEST, false)) { + stopSelf() + } + } + return START_STICKY + } + + fun IsForegroundServiceSupported(): Boolean { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + } + + + override fun onDestroy() { + if (IsForegroundServiceSupported()) { + stopForeground(true) + } + super.onDestroy() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getServiceOngoingNotification(context: Context): Notification { + val notificationChannel = + NotificationChannel("ks_01", "Service Notifications", + NotificationManager.IMPORTANCE_MIN) + notificationChannel.enableLights(false) + notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET + mNotificationManager?.createNotificationChannel(notificationChannel) + + val title = context.getString(R.string.app_running_in_background_notification_title) + val mainActivity = Intent(this, LoginActivity::class.java) + val pendingIntent = + PendingIntent.getActivity(this, 0, mainActivity, 0) + return NotificationCompat.Builder( + context, + "ks_01" + ).setSmallIcon(R.drawable.app_notification_icon) + .setWhen(0) + .setContentTitle(title) + .setOngoing(true) + .setContentIntent(pendingIntent) + .build() + } + + + companion object { + + fun startForegroundService(context: Context) { + Log.d(TAG, "Starting foreground service") + if (SERVICE_START_CALLED) { + return + } + SERVICE_START_CALLED = true + val intent = Intent(context, KitchenSinkForegroundService::class.java) + intent.putExtra(STOP_REQUEST, false) + ContextCompat.startForegroundService(context, intent) + } + + fun stopForegroundService(context: Context) { + Log.d(TAG, "Stopping foreground service") + if (SERVICE_START_CALLED) { + val intent = Intent(context, KitchenSinkForegroundService::class.java) + intent.putExtra(STOP_REQUEST, true) + ContextCompat.startForegroundService(context, intent) + SERVICE_START_CALLED = false + } + } + + private var SERVICE_START_CALLED = false + private const val STOP_REQUEST = "STOP_REQUEST" + private val TAG = "KitchenSinkForegroundService" + } +} \ 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 11b1162..c4c6a1f 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt @@ -10,6 +10,7 @@ import com.ciscowebex.androidsdk.space.Space import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.auth.PhoneServiceRegistrationFailureReason import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus +import com.ciscowebex.androidsdk.auth.UCSSOFailureReason import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeetingObserver import com.ciscowebex.androidsdk.kitchensink.messaging.spaces.listeners.SpaceEventListener @@ -39,7 +40,10 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { ShowNonSSOLogin, OnUCLoginFailed, OnUCLoggedIn, - OnUCServerConnectionStateChanged + OnUCServerConnectionStateChanged, + ShowUCSSOBrowser, + HideUCSSOBrowser, + OnSSOLoginFailed } enum class LogLevel { @@ -341,7 +345,7 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { } // Callbacks - override fun showUCSSOLoginView(ssoUrl: String) { + override fun loadUCSSOViewInBackground(ssoUrl: String) { _cucmLiveData?.postValue(Pair(CucmEvent.ShowSSOLogin, ssoUrl)) Log.d(tag, "showUCSSOLoginView") } @@ -364,9 +368,24 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { } override fun onUCServerConnectionStateChanged(status: UCLoginServerConnectionStatus, failureReason: PhoneServiceRegistrationFailureReason) { - _cucmLiveData?.postValue(Pair(CucmEvent.OnUCServerConnectionStateChanged, "")) Log.d(tag, "onUCServerConnectionStateChanged status: $status failureReason: $failureReason") ucServerConnectionStatus = status ucServerConnectionFailureReason = failureReason + _cucmLiveData?.postValue(Pair(CucmEvent.OnUCServerConnectionStateChanged, "")) + } + + override fun showUCSSOBrowser() { + _cucmLiveData?.postValue(Pair(CucmEvent.ShowUCSSOBrowser, "")) + Log.d(tag, "showUCSSOBrowser") + } + + override fun hideUCSSOBrowser() { + _cucmLiveData?.postValue(Pair(CucmEvent.HideUCSSOBrowser, "")) + Log.d(tag, "hideUCSSOBrowser") + } + + override fun onUCSSOLoginFailed(failureReason: UCSSOFailureReason) { + _cucmLiveData?.postValue(Pair(CucmEvent.OnSSOLoginFailed, failureReason.name)) + Log.d(tag, "onUCSSOLoginFailed : reason = ${failureReason.name}") } } \ No newline at end of file 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 c4dc130..16365ac 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt @@ -546,6 +546,26 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi return webex.getUCServerConnectionStatus() } + fun getUCServerFailureReason(): PhoneServiceRegistrationFailureReason { + return repository.ucServerConnectionFailureReason + } + + fun retryUCSSOLogin() { + webex.retryUCSSOLogin() + } + + fun ucCancelSSOLogin() { + webex.ucCancelSSOLogin() + } + + fun forceRegisterPhoneServices() { + webex.forceRegisterPhoneServices() + } + + fun startUCServices() { + webex.startUCServices() + } + fun startAssociatedCall(callId: String, dialNumber: String, associationType: CallAssociationType, audioCall: Boolean) { getCall(callId)?.startAssociatedCall(dialNumber, associationType, audioCall, CompletionHandler { result -> Log.d(tag, "startAssociatedCall Lambda") @@ -605,11 +625,18 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi _callMembershipsLiveData.postValue(data) var isRemoteSendingAudio = false - data?.forEach { - if (it.getPersonId() != selfPersonId) { - isRemoteSendingAudio = it.isSendingAudio() + + data?.let { + val iterator = it.iterator() + while(iterator.hasNext()) { + val item = iterator.next() + if (item.getPersonId() != selfPersonId) { + if (item.isSendingAudio()) { + isRemoteSendingAudio = true + } + } + repository.participantMuteMap[item.getPersonId()] = item.isSendingAudio() } - repository.participantMuteMap[it.getPersonId()] = it.isSendingAudio() } Log.d(tag, "postParticipantData hasMutedAll: $isRemoteSendingAudio") @@ -996,6 +1023,10 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi getCall(currentCallId.orEmpty())?.setMediaStreamsCategoryB(numStreams, quality) } + fun setMediaStreamCategoryC(participantId: String, quality: MediaStreamQuality) { + getCall(currentCallId.orEmpty())?.setMediaStreamCategoryC(participantId, quality) + } + fun removeMediaStreamCategoryA() { getCall(currentCallId.orEmpty())?.removeMediaStreamCategoryA() } @@ -1004,7 +1035,15 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi getCall(currentCallId.orEmpty())?.removeMediaStreamsCategoryB() } + fun removeMediaStreamCategoryC(participantId: String) { + getCall(currentCallId.orEmpty())?.removeMediaStreamCategoryC(participantId) + } + fun getMediaStreams(): List? { return getCall(currentCallId.orEmpty())?.getMediaStreams() } + + fun isMediaStreamsPinningSupported(): Boolean { + return getCall(currentCallId.orEmpty())?.isMediaStreamsPinningSupported() ?: false + } } \ No newline at end of file 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 c6918cf..84c36a4 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 @@ -17,7 +17,6 @@ import com.ciscowebex.androidsdk.kitchensink.R 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.kitchensink.utils.extensions.toast class CallActivity : BaseActivity(), CallControlsFragment.OnCallActionListener { @@ -135,7 +134,5 @@ class CallActivity : BaseActivity(), CallControlsFragment.OnCallActionListener { Log.d(tag, "hangup error: ${result.error?.errorMessage}") } }) - - } } \ 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 4a618df..affaf3f 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 @@ -36,6 +36,7 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni lateinit var compositeLayoutValue: MediaOption.CompositedVideoLayout lateinit var streamMode: Phone.VideoStreamMode var isSendingVideoForceLandscape: Boolean = false + var multiStreamNewApproach: Boolean = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return BottomSheetCallOptionsBinding.inflate(inflater, container, false).also { binding = it }.apply { @@ -135,7 +136,11 @@ class CallBottomSheetFragment(val showIncomingCallsClickListener: (Call?) -> Uni compositeStream.isEnabled = false compositeStream.alpha = 0.5f compositeLayoutText = getString(R.string.video_stream_mode_multi) - multiStreamOptions.visibility = View.VISIBLE + if (multiStreamNewApproach) { + multiStreamOptions.visibility = View.VISIBLE + } else { + multiStreamOptions.visibility = View.GONE + } } compositeStream.text = compositeLayoutText 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 b7a28fe..65220fe 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 @@ -9,6 +9,7 @@ import android.app.NotificationManager import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.os.Bundle @@ -21,6 +22,7 @@ import android.view.View import android.view.View.OnClickListener import android.view.ViewGroup import android.widget.ImageView +import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast @@ -87,6 +89,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface private lateinit var cameraOptionsDataBottomSheetFragment: CameraOptionsDataBottomSheetFragment private lateinit var multiStreamOptionsBottomSheetFragment: MultiStreamOptionsBottomSheetFragment private lateinit var multiStreamDataOptionsBottomSheetFragment: MultiStreamDataOptionsBottomSheetFragment + private lateinit var mediaStreamBottomSheetFragment: MediaStreamBottomSheetFragment private lateinit var photoViewerBottomSheetFragment: PhotoViewerBottomSheetFragment private lateinit var incomingInfoAdapter: IncomingCallBottomSheetFragment.IncomingInfoAdapter private val mAuxStreamViewMap: HashMap = HashMap() @@ -119,6 +122,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface var audioState: ImageView = item.findViewById(R.id.iv_audio_state) var viewAvatar: ImageView = item.findViewById(R.id.view_avatar) var remoteBorder: RelativeLayout = item.findViewById(R.id.remote_border) + var moreOption: ImageButton = item.findViewById(R.id.ib_more_option) + var streamType: MediaStreamType = MediaStreamType.Unknown + var parentLayout: RelativeLayout = item.findViewById(R.id.parentLayout) + var pinStreamImageView: ImageView = item.findViewById(R.id.iv_pinstream) + var personID: String? = null } companion object { @@ -419,7 +427,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface //remote media stream setRemoteVideoInformation(event.getStream()?.getPerson()?.getDisplayName().orEmpty(), !(event.getStream()?.getPerson()?.isSendingAudio() ?: true)) } else { - val view = getMediaStreamView() + Log.d(TAG, "CallObserver OnMediaChanged MediaStreamAvailabilityEvent personID: ${event.getStream()?.getPerson()?.getPersonId()}," + + "personName: ${event.getStream()?.getPerson()?.getDisplayName()}") + val view = getMediaStreamView(true, event.getStream()?.getStreamType() ?: MediaStreamType.Unknown, + event.getStream()?.getPerson()?.getPersonId()) event.getStream()?.setRenderView(view) val auxStreamViewHolder = mAuxStreamViewMap[view] auxStreamViewHolder?.let { @@ -434,6 +445,12 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } else { it.audioState.setImageResource(R.drawable.ic_microphone_36) } + + if (membership?.isSendingVideo() == true) { + auxStreamViewHolder.viewAvatar.visibility = View.GONE + } else { + auxStreamViewHolder.viewAvatar.visibility = View.VISIBLE + } } } @@ -461,12 +478,15 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged isSendingVideo: ${info.getStream().getPerson()?.isSendingVideo()}") if (info.getStream().getPerson()?.isSendingVideo() == true) { auxStreamViewHolder.viewAvatar.visibility = View.GONE + auxStreamViewHolder.mediaRenderView.visibility = View.VISIBLE + info.getStream().setRenderView(auxStreamViewHolder.mediaRenderView) } else { val membership = info.getStream().getPerson() membership?.let { member -> if (member.getPersonId().isNotEmpty()) { Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged viewAvatar visible") auxStreamViewHolder.viewAvatar.visibility = View.VISIBLE + auxStreamViewHolder.mediaRenderView.visibility = View.GONE } } } @@ -500,16 +520,54 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface " height: " + info.getStream().getSize().height) } + MediaStreamChangeEventType.PinState -> { + val auxStreamViewHolder = mAuxStreamViewMap[info.getStream().getRenderView()] + + if (auxStreamViewHolder != null) { + Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged PinState " + + "isPinned: ${info.getStream().isPinned()} personID: ${info.getStream().getPerson()?.getPersonId()}") + val membership = info.getStream().getPerson() + membership?.let { member -> + if (member.getPersonId().isNotEmpty()) { + Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged PinState getPersonId not empty") + if (isMediaStreamAlreadyPinned(member.getPersonId(), info.getStream().getStreamType())) { + Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged PinState isPinned") + auxStreamViewHolder.pinStreamImageView.visibility = View.VISIBLE + auxStreamViewHolder.parentLayout.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_c) + } else { + auxStreamViewHolder.pinStreamImageView.visibility = View.GONE + auxStreamViewHolder.parentLayout.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_b) + } + } + } + } + } + MediaStreamChangeEventType.Membership -> { - Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged Membership from: ${info.fromMembership()} to: ${info.toMembership()}") + Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged Membership from: ${info.fromMembership().getPersonId()} to: ${info.toMembership().getPersonId()}") val auxStreamViewHolder = mAuxStreamViewMap[info.getStream().getRenderView()] val membership = info.getStream().getPerson() membership?.let { member -> Log.d(tag, "CallObserver OnMediaChanged setOnMediaStreamInfoChanged name: " + member.getDisplayName()) auxStreamViewHolder?.viewAvatar?.visibility = if (member.isSendingVideo()) View.GONE else View.VISIBLE auxStreamViewHolder?.textView?.text = member.getDisplayName() + auxStreamViewHolder?.personID = member.getPersonId() + auxStreamViewHolder?.streamType = info.getStream().getStreamType() + if (isMediaStreamAlreadyPinned(member.getPersonId(), auxStreamViewHolder?.streamType)) { + auxStreamViewHolder?.pinStreamImageView?.visibility = View.VISIBLE + auxStreamViewHolder?.parentLayout?.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_c) + } else { + auxStreamViewHolder?.pinStreamImageView?.visibility = View.GONE + auxStreamViewHolder?.parentLayout?.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_b) + } + + if (info.getStream().getStreamType() == MediaStreamType.Stream1) { + setRemoteVideoInformation(member.getDisplayName().orEmpty(), !(member.isSendingAudio())) + } } } + + else -> {} } } } @@ -976,7 +1034,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface ) callOptionsBottomSheetFragment = CallBottomSheetFragment( - { call -> showIncomingCallBottomSheet(call)}, + { call -> showIncomingCallBottomSheet()}, { call -> showTranscriptions(call) }, { call -> toggleWXAClickListener(call) }, { call -> receivingVideoListener(call) }, @@ -1000,6 +1058,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface { call, quality, duplicate -> categoryAOptionsOkListener(call, quality, duplicate) }, { call, numStreams, quality -> categoryBOptionsOkListener(call, numStreams, quality) } ) + mediaStreamBottomSheetFragment= MediaStreamBottomSheetFragment( + { renderView, personID, quality -> pinStreamClickListener(renderView, personID, quality) }, + { renderView, personID -> unpinStreamClickListener(renderView, personID) } , + { renderView, personID -> closeStreamStreamClickListener(renderView, personID) } ) + initIncomingCallBottomSheet() cameraOptionsBottomSheetFragment = CameraOptionsBottomSheetFragment({ call -> zoomFactorClickListener(call) }, @@ -1303,7 +1366,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface for (model in incomingInfoAdapter.info) { model.isEnabled = true } - showIncomingCallBottomSheet(null) + showIncomingCallBottomSheet() } } } @@ -1318,8 +1381,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface binding.callingHeader.setTextColor(ContextCompat.getColor(requireContext(), R.color.black)) binding.tvName.setTextColor(ContextCompat.getColor(requireContext(), R.color.black)) } else { - binding.callingHeader.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) - binding.tvName.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + val status = isMainStageRemoteUnMuted() + if (status) { + binding.callingHeader.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + binding.tvName.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + } } } @@ -1378,7 +1444,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface if (toHide) { binding.remoteViewLayout.visibility = View.GONE } else { - binding.remoteViewLayout.visibility = View.VISIBLE + val status = isMainStageRemoteUnMuted() + if (status) { + binding.remoteViewLayout.visibility = View.VISIBLE + } } videoViewTextColorState(toHide) @@ -1502,7 +1571,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface override fun onAuxStreamAvailable(): View? { Log.d(tag, "MultiStreamObserver onAuxStreamAvailable") - return getMediaStreamView() + return getMediaStreamView(false, MediaStreamType.Unknown, null) } override fun onAuxStreamUnavailable(): View? { @@ -1544,7 +1613,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface if (webexViewModel.isRemoteVideoMuted) { binding.remoteViewLayout.visibility = View.GONE } else { - binding.remoteViewLayout.visibility = View.VISIBLE + val status = isMainStageRemoteUnMuted() + if (status) { + binding.remoteViewLayout.visibility = View.VISIBLE + } } binding.controlGroup.visibility = View.VISIBLE @@ -1557,13 +1629,55 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } } - private fun getMediaStreamView(): MediaRenderView { + private fun isMediaStreamAlreadyPinned(personID: String?, streamType: MediaStreamType?) : Boolean { + personID?.let { id -> + webexViewModel.getMediaStreams()?.let { streamList -> + + for (stream in streamList) { + Log.d(TAG, "CallControlsFragment isMediaStreamAlreadyPinned personID: $personID, isPinned: ${stream.isPinned()}, streamType: ${stream.getStreamType()}") + } + + val stream = streamList.find { stream -> stream.getStreamType() == streamType } + stream?.let { + return it.isPinned() + } + } + } + + return false + } + + private fun getMediaStreamView(newApproach: Boolean, type: MediaStreamType, personID: String?): MediaRenderView { val auxStreamView: View = LayoutInflater.from(activity).inflate(R.layout.multistream_view, null) val auxStreamViewHolder = AuxStreamViewHolder(auxStreamView) mAuxStreamViewMap[auxStreamViewHolder.mediaRenderView] = auxStreamViewHolder - if (webexViewModel.multistreamNewApproach) { + if (newApproach) { auxStreamViewHolder.audioState.visibility = View.VISIBLE + auxStreamViewHolder.moreOption.visibility = View.VISIBLE + auxStreamViewHolder.streamType = type + auxStreamViewHolder.personID = personID + auxStreamViewHolder.moreOption.tag = auxStreamViewHolder.mediaRenderView + val alreadyPinned = isMediaStreamAlreadyPinned(personID, type) + + Log.d(TAG, "getMediaStreamView personID $personID, alreadyPinned: $alreadyPinned, type: $type") + if (alreadyPinned) { + auxStreamViewHolder.pinStreamImageView.visibility = View.VISIBLE + auxStreamViewHolder.parentLayout.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_c) + } else { + auxStreamViewHolder.pinStreamImageView.visibility = View.GONE + auxStreamViewHolder.parentLayout.background = ContextCompat.getDrawable(requireActivity(), R.drawable.border_category_b) + } + + auxStreamViewHolder.moreOption.setOnClickListener { + val view = auxStreamViewHolder.moreOption.tag as MediaRenderView + val holder = mAuxStreamViewMap[view] + Log.d(TAG, "CallControlsFragment getMediaStreamView moreOption tag: ${holder?.streamType}, personID: ${holder?.personID}") + webexViewModel.currentCallId?.let { + showMediaStreamBottomSheet(webexViewModel.getCall(it), view, holder?.personID, isMediaStreamAlreadyPinned(holder?.personID, holder?.streamType)) + } + } } else { + auxStreamViewHolder.moreOption.visibility = View.GONE auxStreamViewHolder.audioState.visibility = View.GONE } return auxStreamViewHolder.mediaRenderView @@ -1660,23 +1774,27 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface if (webexViewModel.isRemoteScreenShareON) { resizeRemoteVideoView() } - binding.remoteViewLayout.visibility = View.VISIBLE - val pair = webexViewModel.getVideoRenderViews(callId) - if (pair.second == null) { - webexViewModel.setVideoRenderViews(callId, binding.localView, binding.remoteView) - } + val status = isMainStageRemoteUnMuted() + + if (status) { + binding.remoteViewLayout.visibility = View.VISIBLE + val pair = webexViewModel.getVideoRenderViews(callId) + if (pair.second == null) { + webexViewModel.setVideoRenderViews(callId, binding.localView, binding.remoteView) + } - if (webexViewModel.streamMode != Phone.VideoStreamMode.COMPOSITED) { - if (!webexViewModel.multistreamNewApproach) { + if (webexViewModel.streamMode != Phone.VideoStreamMode.COMPOSITED) { + if (!webexViewModel.multistreamNewApproach) { + binding.ivRemoteAudioState.visibility = View.GONE + binding.tvRemoteUserName.visibility = View.GONE + } else { + binding.ivRemoteAudioState.visibility = View.VISIBLE + binding.tvRemoteUserName.visibility = View.VISIBLE + } + } else { binding.ivRemoteAudioState.visibility = View.GONE binding.tvRemoteUserName.visibility = View.GONE - } else { - binding.ivRemoteAudioState.visibility = View.VISIBLE - binding.tvRemoteUserName.visibility = View.VISIBLE } - } else { - binding.ivRemoteAudioState.visibility = View.GONE - binding.tvRemoteUserName.visibility = View.GONE } } @@ -1692,6 +1810,26 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } } + private fun isMainStageRemoteUnMuted() : Boolean { + var status = false + if (!webexViewModel.isRemoteVideoMuted) { + Log.d(TAG, "CallControlsFragment isMainStageRemoteUnMuted isRemoteVideoMuted false") + val streams = webexViewModel.getMediaStreams() + Log.d(TAG, "CallControlsFragment isMainStageRemoteUnMuted streams: ${streams?.size}") + streams?.let { streamList -> + val stream = streamList.find { stream -> stream.getStreamType() == MediaStreamType.Stream1 } + stream?.let { st -> + Log.d(TAG, "CallControlsFragment isMainStageRemoteUnMuted found stream") + status = st.getPerson()?.isSendingVideo() ?: false + } + } ?: run { + status = true + } + } + Log.d(TAG, "CallControlsFragment isMainStageRemoteUnMuted return status: $status") + return status + } + private fun toggleSpeaker(v: View) { v.isSelected = !v.isSelected when { @@ -1713,26 +1851,26 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface internal fun handleFCMIncomingCall(callId: String) { Handler(Looper.getMainLooper()).post { webexViewModel.setFCMIncomingListenerObserver(callId) - onIncomingCall(webexViewModel.getCall(callId)) } } private fun onIncomingCall(call: Call?) { Handler(Looper.getMainLooper()).post { - Log.d(TAG, "CallControlsFragment onIncomingCall callerId: ${call?.getCallId()}, callInfo title: ${call?.getTitle()}") - binding.incomingCallHeader.visibility = View.GONE - val schedules= call?.getSchedules() incomingLayoutState(false) - - schedules?.let { - val item = schedules.first() - if (!checkIncomingAdapterList(item)) { - val model = MeetingInfoModel.convertToMeetingInfoModel(call, item) - incomingInfoAdapter.info.add(model) - Log.d(TAG, "CallControlsFragment onIncomingCall schedules size: ${schedules.size}") + val twentyFourHrsFromNow = Date().time + 86400000 + // Only get meetings till next 24 hours. + val filteredMeetings = schedules?.filter { it.getStart()?.time ?: (twentyFourHrsFromNow + 10) <= twentyFourHrsFromNow} + filteredMeetings?.let { meetings -> + for (meeting in meetings) { + Log.d(TAG,"subject = ${meeting.getSubject()} & meetingId = ${meeting.getId()}") + if (!checkIncomingAdapterList(meeting)) { + val model = MeetingInfoModel.convertToMeetingInfoModel(call, meeting) + incomingInfoAdapter.info.add(model) + Log.d(TAG, "CallControlsFragment onIncomingCall meetings size: ${meetings.size}") + } } } ?: run { val group = call?.isGroupCall() ?: false @@ -1748,19 +1886,19 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface val incomingCalls = incomingInfoAdapter.info.filter { it.call?.getCallId() != webexViewModel.currentCallId } incomingInfoAdapter.info.clear() incomingInfoAdapter.info.addAll(incomingCalls) - - showIncomingCallBottomSheet(call) - incomingInfoAdapter.notifyDataSetChanged() + showIncomingCallBottomSheet() } } - private fun showIncomingCallBottomSheet(call: Call?) { - if (incomingCallBottomSheetFragment.isAdded || incomingCallBottomSheetFragment.isVisible) return + private fun showIncomingCallBottomSheet() { Log.d(TAG, "showIncomingCallBottomSheet") - incomingCallBottomSheetFragment.adapter = incomingInfoAdapter - incomingCallBottomSheetFragment.isCancelable = false - activity?.supportFragmentManager?.let { incomingCallBottomSheetFragment.show(it, IncomingCallBottomSheetFragment.TAG) } - incomingCallBottomSheetFragment.view?.requestLayout() + if (!incomingCallBottomSheetFragment.isAdded && !incomingCallBottomSheetFragment.isVisible) { + incomingCallBottomSheetFragment.adapter = incomingInfoAdapter + incomingCallBottomSheetFragment.isCancelable = false + activity?.supportFragmentManager?.let { incomingCallBottomSheetFragment.show(it, IncomingCallBottomSheetFragment.TAG) } + incomingCallBottomSheetFragment.view?.requestLayout() + } + incomingInfoAdapter.notifyDataSetChanged() } private fun checkIncomingAdapterList(item: CallSchedule): Boolean { @@ -2101,6 +2239,42 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface activity?.supportFragmentManager?.let { multiStreamDataOptionsBottomSheetFragment.show(it, MultiStreamDataOptionsBottomSheetFragment.TAG) } } + private fun showMediaStreamBottomSheet(call: Call?, renderView: MediaRenderView, personID: String?, alreadyPinned: Boolean) { + mediaStreamBottomSheetFragment.call = call + mediaStreamBottomSheetFragment.renderView = renderView + mediaStreamBottomSheetFragment.alreadyPinned = alreadyPinned + mediaStreamBottomSheetFragment.personID = personID + mediaStreamBottomSheetFragment.isMediaStreamsPinningSupported = webexViewModel.isMediaStreamsPinningSupported() + Log.d(TAG, "mediaStreamBottomSheetFragment.isMediaStreamsPinningSupported: ${mediaStreamBottomSheetFragment.isMediaStreamsPinningSupported}") + activity?.supportFragmentManager?.let { mediaStreamBottomSheetFragment.show(it, MediaStreamBottomSheetFragment.TAG) } + } + + private fun pinStreamClickListener(renderView: MediaRenderView?, personID: String?, quality: MediaStreamQuality) { + renderView?.let { view -> + val streams = webexViewModel.getMediaStreams() + streams?.let { streamList -> + val stream = streamList.find { stream -> stream.getPerson()?.getPersonId() == personID } + stream?.let { st -> + st.getPerson()?.let { person -> + Log.d(TAG, "pinStreamClickListener personID $personID, getDisplayName: ${person.getDisplayName()}") + webexViewModel.setMediaStreamCategoryC(person.getPersonId(), quality) + } + } + } + } + } + + private fun unpinStreamClickListener(renderView: MediaRenderView?, personID: String?) { + Log.d(TAG, "unpinStreamClickListener") + renderView?.let { + webexViewModel.removeMediaStreamCategoryC(personID ?: "") + } + } + + private fun closeStreamStreamClickListener(renderView: MediaRenderView?, personID: String?) { + Log.d(TAG, "closeStreamStreamClickListener") + } + private fun showCameraDataOptionsBottomSheetFragment(call: Call?, type: CameraOptionsDataBottomSheetFragment.OptionType, propertyText1: String?, propertyText2: String?, property2Visibility: Boolean) { cameraOptionsDataBottomSheetFragment.call = call cameraOptionsDataBottomSheetFragment.type = type @@ -2247,6 +2421,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface callOptionsBottomSheetFragment.scalingModeValue = webexViewModel.scalingMode callOptionsBottomSheetFragment.compositeLayoutValue = webexViewModel.compositedVideoLayout callOptionsBottomSheetFragment.streamMode = webexViewModel.streamMode + callOptionsBottomSheetFragment.multiStreamNewApproach = webexViewModel.multistreamNewApproach callOptionsBottomSheetFragment.isSendingVideoForceLandscape = webexViewModel.isSendingVideoForceLandscape activity?.supportFragmentManager?.let { callOptionsBottomSheetFragment.show(it, CallBottomSheetFragment.TAG) } } @@ -2280,4 +2455,22 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface Call.MediaQualityInfo.DeviceLimitation -> showDialogWithMessage(requireContext(), R.string.warning, getString(R.string.device_limitation)) } } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + Log.d("CallControlFragment", "newConfig ${newConfig.orientation}" ) + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + binding.localViewLayout.layoutParams.height = + requireActivity().resources.getDimension(R.dimen.local_video_view_width).toInt() + binding.localViewLayout.layoutParams.width = + requireActivity().resources.getDimension(R.dimen.local_video_view_height).toInt() + } + else { + binding.localViewLayout.layoutParams.height = + requireActivity().resources.getDimension(R.dimen.local_video_view_height).toInt() + binding.localViewLayout.layoutParams.width = + requireActivity().resources.getDimension(R.dimen.local_video_view_width).toInt() + } + binding.localViewLayout.requestLayout() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CucmCallActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CucmCallActivity.kt new file mode 100644 index 0000000..e5b7c3e --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CucmCallActivity.kt @@ -0,0 +1,186 @@ +package com.ciscowebex.androidsdk.kitchensink.calling + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus +import com.ciscowebex.androidsdk.kitchensink.KitchenSinkApp +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.WebexRepository +import com.ciscowebex.androidsdk.kitchensink.WebexViewModel +import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityCucmCallBinding +import com.ciscowebex.androidsdk.kitchensink.utils.Constants +import com.ciscowebex.androidsdk.phone.Call +import com.ciscowebex.androidsdk.phone.NotificationCallType +import org.koin.androidx.viewmodel.ext.android.viewModel + +class CucmCallActivity : AppCompatActivity() { + lateinit var binding: ActivityCucmCallBinding + private val webexViewModel: WebexViewModel by viewModel() + private var mCallId: String? = null + private var mPushId: String = "" + private val TAG = "PUSHREST" + + companion object { + fun getIncomingIntent(context: Context, pushId: String? = null): Intent { + val intent = Intent(context, CucmCallActivity::class.java) + intent.putExtra(Constants.Intent.CALLING_ACTIVITY_ID, 1) + intent.putExtra(Constants.Intent.PUSH_ID, pushId) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.action = Constants.Action.WEBEX_CUCM_CALL_ACTION + return intent + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + DataBindingUtil.setContentView(this, R.layout.activity_cucm_call) + .also { binding = it } + .apply { + binding.callerinfo.text = getString(R.string.fetching_details) + binding.callerinfo.visibility = View.VISIBLE + binding.buttongroup.visibility = View.GONE + if (intent.action == Constants.Action.WEBEX_CUCM_CALL_ACTION) { + if (intent?.hasExtra(Constants.Intent.PUSH_ID) == true) { + intent?.getStringExtra(Constants.Intent.PUSH_ID)?.let { pushId -> + mPushId = pushId + Log.i(TAG, "Push call from CUCM $pushId") + handleIncomingCucmCallFromFCM(pushId) + } + } else { + this@CucmCallActivity.finish() + } + + binding.accept.setOnClickListener { + val intent = CallActivity.getIncomingIntent(this@CucmCallActivity) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(Constants.Intent.CALL_ID, mCallId) + intent.action = Constants.Action.WEBEX_CALL_ACTION + startActivity(intent) + this@CucmCallActivity.finishAfterTransition() + } + + binding.decline.setOnClickListener { + this@CucmCallActivity.finish() + } + + } else { + this@CucmCallActivity.finish() + } + } + } + + + private fun handleIncomingCucmCallFromFCM(pushId: String) { + // Initialize koin modules + if (!KitchenSinkApp.isKoinModulesLoaded) { + (application as KitchenSinkApp).loadModules() + } + + if(webexViewModel.webex.authenticator?.isAuthorized() == false){ + webexViewModel.webex.initialize({ result -> + Log.d(TAG, "isAuthorized : ${webexViewModel.webex.authenticator?.isAuthorized()}") + if (result.error != null) { + Log.d(TAG, "errorCode : ${result.error?.errorCode}, errorMessage : ${result.error?.errorMessage}") + runOnUiThread { + finish() + } + } else { + runOnUiThread { + fetchCallDetails(pushId) + } + } + }) + }else{ + runOnUiThread { + fetchCallDetails(pushId) + } + } + } + + + private fun getCallFromPush(pushId: String): Call? { + val actualCallId = webexViewModel.webex.getCallIdByNotificationId(pushId, NotificationCallType.Cucm) + Log.d(TAG, "CallInfo $actualCallId") + val callInfo = webexViewModel.getCall(actualCallId) + Log.d(TAG, "CallInfo ${callInfo?.getCallId()} title ${callInfo?.getTitle()}") + return callInfo + } + + + private fun fetchCallDetails(pushId: String) { + Log.d(TAG, "fetchCallDetails for push $pushId") + webexViewModel.cucmLiveData.observe(this@CucmCallActivity, { + if (it != null) { + Log.d(TAG, "CUCM Event : ${it.first.name} ${webexViewModel.ucServerConnectionStatus}") + when (WebexRepository.CucmEvent.valueOf(it.first.name)) { + WebexRepository.CucmEvent.OnUCLoggedIn -> { + Log.d(TAG, "UC Login completed") + } + WebexRepository.CucmEvent.OnUCServerConnectionStateChanged -> { + handleUCConnectionStateChange() + } + else -> { + Log.d(TAG, "CUCM Event details : ${WebexRepository.CucmEvent.valueOf(it.first.name)}") + } + } + } else { + Log.d(TAG, "CUCM Event details : null") + } + }) + + Handler(Looper.getMainLooper()).post { + Log.d(TAG, "Starting UC services") + webexViewModel.startUCServices() + handleUCConnectionStateChange() + } + } + + private fun handleUCConnectionStateChange(){ + if(mPushId.isEmpty()){ + return + } + when (webexViewModel.ucServerConnectionStatus) { + UCLoginServerConnectionStatus.Connected -> { + Log.d(TAG, "Phone services connected") + binding.ucServerConnectionStatusTextView.text = resources.getString(R.string.phone_service_connected) + binding.ucServerConnectionStatusTextView.visibility = View.VISIBLE + Handler(Looper.getMainLooper()).postDelayed({ + val call = getCallFromPush(mPushId) + mCallId = call?.getCallId() + if (mCallId == null) { + binding.callerinfo.text = getString(R.string.caller_details_unavailable) + binding.callerinfo.visibility = View.VISIBLE + binding.buttongroup.visibility = View.VISIBLE + binding.accept.visibility = View.GONE + } else { + binding.callerinfo.text = call?.getTitle() + binding.callerinfo.visibility = View.VISIBLE + binding.buttongroup.visibility = View.VISIBLE + binding.accept.visibility = View.VISIBLE + binding.decline.visibility = View.VISIBLE + } + }, 10) + } + UCLoginServerConnectionStatus.Failed -> { + val text = resources.getString(R.string.phone_service_failed) + " " + webexViewModel.ucServerConnectionFailureReason + binding.ucServerConnectionStatusTextView.text = text + binding.ucServerConnectionStatusTextView.visibility = View.VISIBLE + } + else -> { + binding.ucServerConnectionStatusTextView.visibility = View.GONE + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/MediaStreamBottomSheetFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/MediaStreamBottomSheetFragment.kt new file mode 100644 index 0000000..4c2c33d --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/MediaStreamBottomSheetFragment.kt @@ -0,0 +1,99 @@ +package com.ciscowebex.androidsdk.kitchensink.calling + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetMediaStreamBinding +import com.ciscowebex.androidsdk.phone.Call +import com.ciscowebex.androidsdk.phone.MediaRenderView +import com.ciscowebex.androidsdk.phone.MediaStreamQuality +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class MediaStreamBottomSheetFragment(val pinStreamClickListener: (MediaRenderView?, String?, MediaStreamQuality) -> Unit, + val unpinStreamClickListener: (MediaRenderView?, String?) -> Unit, + val closeStreamStreamClickListener: (MediaRenderView?, String?) -> Unit): BottomSheetDialogFragment() { + companion object { + val TAG = "MediaStreamBottomSheetFragment" + } + + private lateinit var binding: BottomSheetMediaStreamBinding + var call: Call? = null + var renderView: MediaRenderView? = null + var personID: String? = null + var alreadyPinned: Boolean = false + var isMediaStreamsPinningSupported: Boolean = false + private var streamQuality = MediaStreamQuality.LD + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return BottomSheetMediaStreamBinding.inflate(inflater, container, false).also { binding = it }.apply { + + hidePinOptions() + + Log.d("MediaStreamBottomSheetFragment", "isMediaStreamsPinningSupported: $isMediaStreamsPinningSupported, personID: $personID" + + "alreadyPinned: $alreadyPinned") + + if (isMediaStreamsPinningSupported) { + if (alreadyPinned) { + pinStream.visibility = View.GONE + unpinStream.visibility = View.VISIBLE + } else { + unpinStream.visibility = View.GONE + pinStream.visibility = View.VISIBLE + } + } else { + pinStream.visibility = View.GONE + unpinStream.visibility = View.GONE + } + + pinStream.setOnClickListener { + showPinOptions() + } + + unpinStream.setOnClickListener { + dismiss() + unpinStreamClickListener(renderView, personID) + } + + qualitySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { + val quality = resources.getStringArray(R.array.qualityArray)[position] + Log.d(tag, "selected quality $quality") + when (quality) { + "LD" -> streamQuality = MediaStreamQuality.LD + "SD" -> streamQuality = MediaStreamQuality.SD + "HD" -> streamQuality = MediaStreamQuality.HD + "FHD" -> streamQuality = MediaStreamQuality.FHD + } + } + } + + qualitySpinner.setSelection(resources.getStringArray(R.array.qualityArray).indexOf(streamQuality.name)) + + ok.setOnClickListener { + dismiss() + pinStreamClickListener(renderView, personID, streamQuality) + } + + cancel.setOnClickListener { dismiss() } + }.root + } + + private fun showPinOptions() { + binding.OptionsRelLayout.visibility = View.GONE + binding.ok.visibility = View.VISIBLE + binding.propertyOptionsRelLayout.visibility = View.VISIBLE + } + + private fun hidePinOptions() { + binding.ok.visibility = View.GONE + binding.propertyOptionsRelLayout.visibility = View.GONE + binding.OptionsRelLayout.visibility = View.VISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/cucm/UCLoginActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/cucm/UCLoginActivity.kt index 1a2d127..8a570ac 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/cucm/UCLoginActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/cucm/UCLoginActivity.kt @@ -1,13 +1,11 @@ package com.ciscowebex.androidsdk.kitchensink.cucm - import android.app.AlertDialog import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import android.view.View -import android.widget.Toast import androidx.databinding.DataBindingUtil import androidx.lifecycle.Observer import com.ciscowebex.androidsdk.auth.UCSSOWebViewAuthenticator @@ -18,7 +16,10 @@ import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityCucmLoginBindin import com.ciscowebex.androidsdk.kitchensink.databinding.DialogUcloginNonssoBinding import com.ciscowebex.androidsdk.kitchensink.databinding.DialogUcloginSettingsBinding import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.auth.PhoneServiceRegistrationFailureReason import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus +import com.ciscowebex.androidsdk.kitchensink.KitchenSinkApp.Companion.isUCSSOLogin +import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage class UCLoginActivity : BaseActivity() { @@ -27,6 +28,8 @@ class UCLoginActivity : BaseActivity() { private var nonSSOAlertDialog: AlertDialog? = null private var ucSettingsAlertDialog: AlertDialog? = null + var isUCSSOLoginSuccessful = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) tag = "UCLoginActivity" @@ -37,13 +40,14 @@ class UCLoginActivity : BaseActivity() { if (it != null) { when (WebexRepository.CucmEvent.valueOf(it.first.name)) { WebexRepository.CucmEvent.ShowSSOLogin -> { - + Log.d(tag, "Callback : Show sso login with url : ${it.second}") progressBar.visibility = View.GONE ssologinWebview.visibility = View.VISIBLE nonSSOAlertDialog?.dismiss() ucSettingsAlertDialog?.dismiss() + isUCSSOLogin = true UCSSOWebViewAuthenticator.launchWebView(ssologinWebview, it.second, CompletionHandler { result -> if (result.isSuccessful) { Log.d(tag, "UCLoginActivity SSO login Successful") @@ -51,24 +55,61 @@ class UCLoginActivity : BaseActivity() { Handler(Looper.getMainLooper()).post { ssologinWebview.visibility = View.GONE progressBar.visibility = View.VISIBLE + updateUCLoginStatusUI(getString(R.string.uc_login_success)) } } else { Log.d(tag, "UCLoginActivity SSO login Failed") - ucLoginEvent(getString(R.string.uc_login_failed)) + Handler(Looper.getMainLooper()).post { + binding.ssologinWebview.visibility = View.GONE + binding.progressBar.visibility = View.GONE + updateUCLoginStatusUI(getString(R.string.uc_login_failed)) + } } }) } WebexRepository.CucmEvent.ShowNonSSOLogin -> { + Log.d(tag, "Callback : Show non sso login") showUCNonSSOLoginDialog() } WebexRepository.CucmEvent.OnUCLoggedIn -> { - ucLoginEvent(getString(R.string.uc_login_success)) + Log.d(tag, "Callback : Uc logged in") + Handler(Looper.getMainLooper()).post { + binding.ssologinWebview.visibility = View.GONE + updateUCLoginStatusUI(getString(R.string.uc_login_success)) + } } WebexRepository.CucmEvent.OnUCLoginFailed -> { - ucLoginEvent(getString(R.string.uc_login_failed)) + Log.d(tag, "Callback : Uc login failed") + Handler(Looper.getMainLooper()).post { + binding.ssologinWebview.visibility = View.GONE + binding.progressBar.visibility = View.GONE + updateUCLoginStatusUI(getString(R.string.uc_login_failed)) + } } WebexRepository.CucmEvent.OnUCServerConnectionStateChanged -> { - processServerConnectionStatus(webexViewModel.getUCServerConnectionStatus()) + Log.d(tag, "Callback : Uc server connection state changed") + processServerConnectionStatus(webexViewModel.getUCServerConnectionStatus(), webexViewModel.getUCServerFailureReason()) + } + WebexRepository.CucmEvent.ShowUCSSOBrowser -> { + Log.d(tag, "ShowUCSSOBrowser") + ssologinWebview.visibility = View.VISIBLE + } + WebexRepository.CucmEvent.HideUCSSOBrowser -> { + Log.d(tag, "HideUCSSOBrowser") + isUCSSOLoginSuccessful = true + ssologinWebview.visibility = View.GONE + } + WebexRepository.CucmEvent.OnSSOLoginFailed -> { + Log.d(tag, "Callback : OnSSOLoginFailed") + showDialogWithMessage(this@UCLoginActivity, getString(R.string.login_failed), "Reason : ${it.second}", R.string.retry, true, + { dialog, _ -> + dialog.dismiss() + webexViewModel.retryUCSSOLogin() + }, + R.string.ok, + { dialog, _ -> + dialog.dismiss() + }) } } } @@ -77,8 +118,17 @@ class UCLoginActivity : BaseActivity() { progressBar.visibility = View.VISIBLE Handler(Looper.getMainLooper()).post { - if (webexViewModel.isUCLoggedIn()) { - ucLoginEvent(getString(R.string.uc_login_success)) + webexViewModel.startUCServices() + if (isUCSSOLogin && !webexViewModel.isUCLoggedIn()) { + Log.d(tag, "isUCSSOLogin && !webexViewModel.isUCLoggedIn() -> retrying sso login") + // To handle the case, when user starts UCSSOLogin but stops midway before success and presses back + webexViewModel.retryUCSSOLogin() + } else if (webexViewModel.isUCLoggedIn()) { + Handler(Looper.getMainLooper()).post { + binding.ssologinWebview.visibility = View.GONE + binding.progressBar.visibility = View.GONE + updateUCLoginStatusUI(getString(R.string.uc_login_success)) + } } else { showUCLoginSettingsDialog() } @@ -86,31 +136,65 @@ class UCLoginActivity : BaseActivity() { } } - private fun processServerConnectionStatus(status: UCLoginServerConnectionStatus) { - Log.d(tag, "processServerConnectionStatus status: $status") - when (status) { - UCLoginServerConnectionStatus.Idle -> {} - UCLoginServerConnectionStatus.Connecting -> {} - UCLoginServerConnectionStatus.Connected -> { - ucLoginEvent(getString(R.string.uc_server_connected)) - } - UCLoginServerConnectionStatus.Disconnected -> {} - UCLoginServerConnectionStatus.Failed -> {} + override fun onBackPressed() { + if (isUCSSOLogin && !isUCSSOLoginSuccessful) { + Log.d(tag, "ucCancelSSOLogin()") + webexViewModel.ucCancelSSOLogin() } + super.onBackPressed() } - private fun ucLoginEvent(message: String) { - Handler(Looper.getMainLooper()).post { - binding.ssologinWebview.visibility = View.GONE - binding.progressBar.visibility = View.GONE - showToast(message) + private fun processServerConnectionStatus( + status: UCLoginServerConnectionStatus, + ucServerFailureReason: PhoneServiceRegistrationFailureReason + ) { + Log.d(tag, "processServerConnectionStatus status: $status, failureReason : $ucServerFailureReason") + if (ucServerFailureReason != PhoneServiceRegistrationFailureReason.None) { + if (ucServerFailureReason == PhoneServiceRegistrationFailureReason.RegisteredElsewhere) { + showDialogWithMessage(this@UCLoginActivity, getString(R.string.force_register), getString(R.string.force_register_dialog_message), R.string.yes, true, + { dialog, _ -> + dialog.dismiss() + webexViewModel.forceRegisterPhoneServices() + }, + R.string.no, + { dialog, _ -> + dialog.dismiss() + }) + } + Handler(Looper.getMainLooper()).post { + updatePhoneServiceConnectionUI("Phone services connection failed : ${ucServerFailureReason.name}") + binding.ssologinWebview.visibility = View.GONE + binding.progressBar.visibility = View.GONE + } + + } else { + Handler(Looper.getMainLooper()).post { + updatePhoneServiceConnectionUI("Phone services state : ${status.name}") + } + when (status) { + UCLoginServerConnectionStatus.Connected -> { + Handler(Looper.getMainLooper()).post { + binding.ssologinWebview.visibility = View.GONE + binding.progressBar.visibility = View.GONE + ucSettingsAlertDialog?.dismiss() + } + } + UCLoginServerConnectionStatus.Failed -> { + Handler(Looper.getMainLooper()).post { + binding.progressBar.visibility = View.GONE + } + } + else -> {} + } } } - private fun showToast(message: String) { - val toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT) - toast.show() - updateUCData() + private fun updatePhoneServiceConnectionUI(status: String) { + binding.ucServerConnectionStatusTextView.text = status + } + + private fun updateUCLoginStatusUI(status: String) { + binding.ucLoginStatusTextView.text = status } private fun setUCDomainServerUrl(domain: String, serverUrl: String) { @@ -165,28 +249,4 @@ class UCLoginActivity : BaseActivity() { nonSSOAlertDialog?.setCanceledOnTouchOutside(false) nonSSOAlertDialog?.show() } - - private fun updateUCData() { - Log.d(tag, "updateUCData isCUCMServerLoggedIn: ${webexViewModel.repository.isCUCMServerLoggedIn} ucServerConnectionStatus: ${webexViewModel.repository.ucServerConnectionStatus}") - if (webexViewModel.isCUCMServerLoggedIn) { - binding.ucLoginStatusTextView.visibility = View.VISIBLE - } else { - binding.ucLoginStatusTextView.visibility = View.GONE - } - - when (webexViewModel.ucServerConnectionStatus) { - UCLoginServerConnectionStatus.Connected -> { - binding.ucServerConnectionStatusTextView.text = resources.getString(R.string.phone_service_connected) - binding.ucServerConnectionStatusTextView.visibility = View.VISIBLE - } - UCLoginServerConnectionStatus.Failed -> { - val text = resources.getString(R.string.phone_service_failed) + " " + webexViewModel.ucServerConnectionFailureReason - binding.ucServerConnectionStatusTextView.text = text - binding.ucServerConnectionStatusTextView.visibility = View.VISIBLE - } - else -> { - binding.ucServerConnectionStatusTextView.visibility = View.GONE - } - } - } } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/firebase/KitchenSinkFCMService.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/firebase/KitchenSinkFCMService.kt index b796794..28a776a 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/firebase/KitchenSinkFCMService.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/firebase/KitchenSinkFCMService.kt @@ -12,10 +12,13 @@ import android.os.Looper import android.text.Html import android.util.Log import androidx.core.app.NotificationCompat +import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.kitchensink.HomeActivity +import com.ciscowebex.androidsdk.kitchensink.KitchenSinkApp import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.WebexRepository import com.ciscowebex.androidsdk.kitchensink.calling.CallActivity +import com.ciscowebex.androidsdk.kitchensink.calling.CucmCallActivity import com.ciscowebex.androidsdk.kitchensink.firebase.KitchenSinkFCMService.WebhookResources.CALL_MEMBERSHIPS import com.ciscowebex.androidsdk.kitchensink.firebase.KitchenSinkFCMService.WebhookResources.MESSAGES import com.ciscowebex.androidsdk.kitchensink.utils.Base64Utils @@ -23,12 +26,10 @@ import com.ciscowebex.androidsdk.kitchensink.utils.Constants import com.ciscowebex.androidsdk.kitchensink.utils.decryptPushRESTPayload import com.ciscowebex.androidsdk.message.Message import com.ciscowebex.androidsdk.phone.Call -import com.ciscowebex.androidsdk.CompletionHandler -import com.ciscowebex.androidsdk.kitchensink.KitchenSinkApp +import com.ciscowebex.androidsdk.phone.NotificationCallType import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.google.gson.Gson -import com.ciscowebex.androidsdk.phone.NotificationCallType import org.json.JSONObject import org.koin.android.ext.android.inject import kotlin.random.Random @@ -38,33 +39,32 @@ class KitchenSinkFCMService : FirebaseMessagingService() { private val repository: WebexRepository by inject() - - override fun onMessageReceived(remoteMessage: RemoteMessage) { - - Log.d(TAG, "From: " + remoteMessage.from) - Log.d(TAG, "APP isInForeground: " + KitchenSinkApp.inForeground) - if (KitchenSinkApp.inForeground) return - + private fun processFCMMessage(remoteMessage: RemoteMessage){ var notificationData: FCMPushModel? - if (remoteMessage.data.isNotEmpty()) { + // CUCM Push Rest Flow val map = remoteMessage.data val pushRestPayload = map["body"] if (!pushRestPayload.isNullOrEmpty()) { - // This FCM notification is generated by PushREST -// sample payload: eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..pnhaRj0e109Khb1j.mXZBfDQMj_c4dFaZRwRuVSOI0LcrZRvpnoBknDQDsYKsDVQtIppi1y7cWBsQ8doNLs-Cp6UEkzLlOX_SHOLqYhdHdfo8n5-nRfTI0gUx72UQtvuPGBFKUStU_B7TQmEBs7OQBClHjUNiTIo_Q70NTijE0ErgUzXhpXVHtgDnMW79HDzJ37Y4PUM96ssd8uY7WZuezTKkDYAjVYutQ5-MBe2z3oaFeXqy1hgfWVJY_y2L9eC7RHaMkUFmONaNmiryTssxcp1aWkWOqyMWNlu6igh1.Wy3QMt5_loajfrHrCCyfzQ - Log.d(TAG, "Payload from PushREST : $pushRestPayload") - - // Sample encryption logic - //val dummyPayload = "This is a dummyP@yload! for Testing" - //val encryptedPayload = encryptPushRESTPayload(dummyPayload) + Handler(Looper.getMainLooper()).post{ + if(repository.webex.authenticator?.isAuthorized() == false) { + repository.webex.initialize { result -> + if (result.error == null) { + Log.d(TAG, "Starting UC services in FCM service") + repository.webex.startUCServices() + } + } + }else{ + Log.d(TAG, "Starting UC services in FCM service") + repository.webex.startUCServices() + } + } - // Decrypt using key + Log.d(TAG, "Payload from PushREST : $pushRestPayload") val decryptedPayload = decryptPushRESTPayload(pushRestPayload) Log.d(TAG, "Decrypted payload : $decryptedPayload") val pushRestPayloadJson = getPushRestPayloadModel(decryptedPayload) buildCallNotification(pushRestPayloadJson) - } else { // FCM triggered via webhook from push notification server val data = map["data"] @@ -89,7 +89,19 @@ class KitchenSinkFCMService : FirebaseMessagingService() { } } } + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + Log.d(TAG, "From: " + remoteMessage.from) + Log.d(TAG, "APP chg isInForeground: " + KitchenSinkApp.inForeground) + if (KitchenSinkApp.inForeground) return + + if(!(application as KitchenSinkApp).loadModules()){ + Log.w(TAG, "Login type unknown") + return + } + processFCMMessage(remoteMessage) } private fun buildCallNotification(data: FCMPushModel) { @@ -100,26 +112,22 @@ class KitchenSinkFCMService : FirebaseMessagingService() { Log.d(TAG, "CallInfo ${callInfo?.getCallId()} title ${callInfo?.getTitle()}") sendCallNotification(callInfo) }, 100) - } private fun buildCallNotification(data: PushRestPayloadModel) { Handler(Looper.getMainLooper()).postDelayed({ if(data.pushid != null){ - val actualCallId = repository.getCallIdByNotificationId(data.pushid, NotificationCallType.Cucm) - val callInfo = repository.getCall(actualCallId) - Log.d(TAG, "CallInfo ${callInfo?.getCallId()} title ${callInfo?.getTitle()}") + Log.d(TAG, "Pushid is "+data.pushid); //CUCM flow if (data.type == "incomingcall") //data.type = incomingcall,missedcall - sendCallNotification(callInfo, data.displayname) + sendCucmCallNotification(data.pushid, data.displaynumber) }else { Log.d(TAG, "Push id is null") } - }, 100) + }, 10) } private fun sendCallNotification(callInfo: Call?, caller: String? = null) { - val callTitle = caller ?: callInfo?.getTitle() val notificationId = Random.nextInt(10000) val requestCode = Random.nextInt(10000) val intent = CallActivity.getIncomingIntent(this) @@ -134,7 +142,36 @@ class KitchenSinkFCMService : FirebaseMessagingService() { val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val notificationBuilder = NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.app_notification_icon) - .setContentTitle("$callTitle is calling") + .setContentTitle("$caller is calling") + .setContentText(getString(R.string.call_description)) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setContentIntent(pendingIntent) + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? + + // Since android Oreo notification channel is needed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(channelId, + WEBEX_CALL_CHANNEL, + NotificationManager.IMPORTANCE_DEFAULT) + notificationManager?.createNotificationChannel(channel) + } + notificationManager?.notify(notificationId, notificationBuilder.build()) + } + + private fun sendCucmCallNotification(pushId: String?, caller: String? = null) { + val notificationId = Random.nextInt(10000) + val requestCode = Random.nextInt(10000) + val intent = CucmCallActivity.getIncomingIntent(this, pushId) + + + val pendingIntent = PendingIntent.getActivity(this, requestCode, intent, + PendingIntent.FLAG_ONE_SHOT) + val channelId: String = getString(R.string.default_notification_channel_id) + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val notificationBuilder = NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.app_notification_icon) + .setContentTitle("$caller is calling") .setContentText(getString(R.string.call_description)) .setAutoCancel(true) .setSound(defaultSoundUri) @@ -232,7 +269,7 @@ class KitchenSinkFCMService : FirebaseMessagingService() { } companion object { - private const val TAG = "MyFirebaseMsgService" + private const val TAG = "PUSHREST" private const val WEBEX_CALL_CHANNEL = "WebexCallChannel" private const val CUCM_CALL_CHANNEL = "CUCMCallChannel" private const val MESSAGE_CHANNEL = "MessageChannel" diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/composer/MessageComposerActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/composer/MessageComposerActivity.kt index 4bc6ad4..499ff10 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/composer/MessageComposerActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/composer/MessageComposerActivity.kt @@ -229,7 +229,8 @@ class MessageComposerActivity : AppCompatActivity() { for (file in attachmentAdapter.attachedFiles) { var thumbnail: LocalFile.Thumbnail? = null - if (MimeUtils.getContentTypeByFilename(file.name) == MimeUtils.ContentType.IMAGE) { + //To get the thumbnail of the image and video, provide the same file in the thumbnail field also. SDK will process the data to fetch the thumbnail for the image and video + if (MimeUtils.getContentTypeByFilename(file.name) == MimeUtils.ContentType.IMAGE || MimeUtils.getContentTypeByFilename(file.name) == MimeUtils.ContentType.VIDEO) { thumbnail = LocalFile.Thumbnail(file, null, resources.getInteger(R.integer.attachment_thumbnail_width), resources.getInteger(R.integer.attachment_thumbnail_height)) } val localFile = LocalFile(file, null, thumbnail, null) diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesFragment.kt index 5a73d5e..45c1359 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesFragment.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesFragment.kt @@ -81,10 +81,10 @@ class SpacesFragment : Fragment() { override fun onCreate(space: Space) { val spaceModel = SpaceModel.convertToSpaceModel(space) - spacesClientAdapter.spaces.add(0, spaceModel) + spacesClientAdapter.spaces.add(spaceModel) Log.d(TAG, "Space event ${space.title} is created") activity?.runOnUiThread { - spacesClientAdapter.notifyItemInserted(0) + spacesClientAdapter.notifyItemInserted(spacesClientAdapter.spaces.size - 1) } } @@ -199,6 +199,7 @@ class SpacesFragment : Fragment() { spacesViewModel.spaces.observe(this@SpacesFragment.viewLifecycleOwner, Observer { spaces -> spaces?.let { + Log.d(TAG, "number of spaces obtained : ${it.size}") binding.swipeContainer.isRefreshing = false spacesClientAdapter.spaces.clear() diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesRepository.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesRepository.kt index f5e474e..b02b2e3 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesRepository.kt @@ -8,6 +8,7 @@ import com.ciscowebex.androidsdk.space.Space.SpaceType import com.ciscowebex.androidsdk.space.SpaceClient.SortBy import io.reactivex.Observable import io.reactivex.Single +import java.util.Date class SpacesRepository(private val webex: Webex) : MessagingRepository(webex) { fun fetchSpacesList(teamId: String?, maxSpaces: Int, sortBy: SortBy): Observable> { @@ -88,10 +89,11 @@ class SpacesRepository(private val webex: Webex) : MessagingRepository(webex) { }.toObservable() } - fun listMessages(spaceId: String, beforeMessageId: String? = null): Observable> { + fun listMessages(spaceId: String, beforeMessageId: String? = null, beforeMessageDate: Long? = null): Observable> { return Single.create> { emitter -> var before: Before? = null beforeMessageId?.let { before = Before.Message(it) } + beforeMessageDate?.let { before = Before.Date(Date(it)) } webex.messages.list(spaceId, before, 100, null, CompletionHandler { result -> if (result.isSuccessful) { emitter.onSuccess(result.data?.map { SpaceMessageModel.convertToSpaceMessageModel(it) }.orEmpty()) diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesViewModel.kt index 0f95cf9..e7b8361 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/SpacesViewModel.kt @@ -42,6 +42,8 @@ class SpacesViewModel(private val spacesRepo: SpacesRepository, private val addOnCallSuffix = " (On Call)" + private val TAG = "SpacesViewModel" + override fun onCleared() { webexRepository.clearSpaceData() } @@ -49,16 +51,22 @@ class SpacesViewModel(private val spacesRepo: SpacesRepository, private fun getSpacesWithActiveCalls() { val allSpaces = arrayListOf() spacesRepo.listSpacesWithActiveCalls().observeOn(AndroidSchedulers.mainThread()).subscribe({ spaceIds -> - spaces.value?.forEach { space -> - if(spaceIds.contains(space.id)) { - val tempSpace = SpaceModel(space.id, space.title + addOnCallSuffix, space.spaceType, space.isLocked, space.lastActivity, space.created, space.teamId, space.sipAddress) - allSpaces.add(tempSpace) - } else { - allSpaces.add(space) + Log.d(TAG, "listSpacesWithActiveCalls spaceIds.size = ${spaceIds.size}") + if (spaceIds.isNotEmpty()) { + spaces.value?.forEach { space -> + if(spaceIds.contains(space.id)) { + val tempSpace = SpaceModel(space.id, space.title + addOnCallSuffix, space.spaceType, space.isLocked, space.lastActivity, space.created, space.teamId, space.sipAddress) + allSpaces.add(tempSpace) + } else { + allSpaces.add(space) + } } + _spaces.postValue(allSpaces) } - _spaces.postValue(allSpaces) - }) { _spaces.postValue(spaces.value)}.autoDispose() + }) + { + Log.e(TAG, "error in listSpacesWithActiveCalls : ${it.message}") + }.autoDispose() } fun setSpaceEventListener(listener : SpaceEventListener) { diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/MessageActionBottomSheetFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/MessageActionBottomSheetFragment.kt index f44a4b9..c4ecaf9 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/MessageActionBottomSheetFragment.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/MessageActionBottomSheetFragment.kt @@ -13,7 +13,8 @@ class MessageActionBottomSheetFragment(val deleteMessageClickListener: (SpaceMes val markMessageAsReadClickListener: (SpaceMessageModel) -> Unit, val replyMessageClickListener: (SpaceMessageModel) -> Unit, val editMessageClickListener: (SpaceMessageModel) -> Unit, - val fetchByIdClickListener: (SpaceMessageModel) -> Unit) : BottomSheetDialogFragment() { + val fetchByIdClickListener: (SpaceMessageModel) -> Unit, + val fetchByDateClickListener: (SpaceMessageModel) -> Unit) : BottomSheetDialogFragment() { companion object { val TAG = "MessageActionBottomSheetFragment" var selfPersonId : String? = null @@ -62,6 +63,11 @@ class MessageActionBottomSheetFragment(val deleteMessageClickListener: (SpaceMes fetchByIdClickListener(message) } + fetchByDate.setOnClickListener { + dismiss() + fetchByDateClickListener(message) + } + cancel.setOnClickListener { dismiss() } }.root } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailActivity.kt index b6eced4..6389bc0 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailActivity.kt @@ -56,7 +56,8 @@ class SpaceDetailActivity : BaseActivity() { { message -> spaceDetailViewModel.markMessageAsRead(message) }, { message -> replyMessageListener(message) }, { message -> editMessage(message)}, - { message -> fetchMessageBeforeMessageId(message)}) + { message -> fetchMessageBeforeMessageId(message)}, + { message -> fetchMessageBeforeDate(message)}) messageClientAdapter = MessageClientAdapter(messageActionBottomSheetFragment, supportFragmentManager) spaceMessageRecyclerView.adapter = messageClientAdapter @@ -98,6 +99,10 @@ class SpaceDetailActivity : BaseActivity() { spaceDetailViewModel.getMessages(message.messageId) } + private fun fetchMessageBeforeDate(message: SpaceMessageModel) { + spaceDetailViewModel.getMessages(null, message.mMessage?.getCreated() ?: 0L) + } + override fun onResume() { super.onResume() spaceDetailViewModel.getSpaceById() diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailViewModel.kt index c005489..e6fe090 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/messaging/spaces/detail/SpaceDetailViewModel.kt @@ -71,8 +71,8 @@ class SpaceDetailViewModel(private val spacesRepo: SpacesRepository, private val }, { _space.postValue(null) }).autoDispose() } - fun getMessages(beforeMessageId: String? = null) { - spacesRepo.listMessages(spaceId, beforeMessageId).observeOn(AndroidSchedulers.mainThread()).subscribe({ messageModels -> + fun getMessages(beforeMessageId: String? = null, beforeMessageDate: Long? = null) { + spacesRepo.listMessages(spaceId, beforeMessageId, beforeMessageDate).observeOn(AndroidSchedulers.mainThread()).subscribe({ messageModels -> _spaceMessages.postValue(messageModels) }, { _spaceMessages.postValue(emptyList()) }).autoDispose() } 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 8bf1f01..830f68a 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 @@ -15,11 +15,11 @@ import com.ciscowebex.androidsdk.kitchensink.WebexRepository import com.ciscowebex.androidsdk.kitchensink.calling.CallActivity import com.ciscowebex.androidsdk.kitchensink.databinding.CommonFragmentItemListBinding import com.ciscowebex.androidsdk.kitchensink.databinding.FragmentCommonBinding -import com.ciscowebex.androidsdk.kitchensink.messaging.spaces.SpaceModel import com.ciscowebex.androidsdk.kitchensink.utils.Constants -import com.ciscowebex.androidsdk.space.Space -import kotlinx.android.synthetic.main.fragment_common.* +import com.ciscowebex.androidsdk.kitchensink.utils.formatCallDurationTime +import com.ciscowebex.androidsdk.phone.CallHistoryRecord import org.koin.android.ext.android.inject +import java.text.SimpleDateFormat class SearchCommonFragment : Fragment() { @@ -27,6 +27,7 @@ class SearchCommonFragment : Fragment() { private var adapter: CustomAdapter = CustomAdapter() private val itemModelList = mutableListOf() lateinit var taskType: String + lateinit var binding: FragmentCommonBinding companion object { object TaskType { @@ -41,7 +42,9 @@ class SearchCommonFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - return FragmentCommonBinding.inflate(inflater, container, false).apply { + return FragmentCommonBinding.inflate(inflater, container, false) + .also { binding = it } + .apply { lifecycleOwner = this@SearchCommonFragment recyclerView.itemAnimator = DefaultItemAnimator() @@ -57,7 +60,7 @@ class SearchCommonFragment : Fragment() { } override fun onQueryTextChange(newText: String?): Boolean { - progress_bar.visibility = View.VISIBLE + progressBar.visibility = View.VISIBLE searchViewModel.search(newText) return false } @@ -73,7 +76,7 @@ class SearchCommonFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) updateSearchInputViewVisibility() - progress_bar.visibility = View.VISIBLE + binding.progressBar.visibility = View.VISIBLE } override fun onResume() { @@ -86,7 +89,6 @@ class SearchCommonFragment : Fragment() { searchViewModel.spaces.observe(viewLifecycleOwner, Observer { list -> list?.let { if (taskType == TaskType.TaskCallHistory) it.sortedBy { it.created } else it.sortedByDescending { it.lastActivity } - if (it.isEmpty()) { updateEmptyListUI(true) } else { @@ -111,6 +113,34 @@ class SearchCommonFragment : Fragment() { } }) + searchViewModel.callHistoryRecords.observe(viewLifecycleOwner, Observer { list -> + list?.let { + if (it.isEmpty()) { + updateEmptyListUI(true) + } else { + updateEmptyListUI(false) + itemModelList.clear() + for (i in it.indices) { + val itemModel = ItemModel() + val callRecord = it[i] + itemModel.name = callRecord.displayName.orEmpty() + itemModel.image = R.drawable.ic_call + itemModel.callerId = callRecord.callbackAddress.orEmpty() + itemModel.ongoing = searchViewModel.isSpaceCallStarted() && searchViewModel.spaceCallId() == callRecord.conversationId +// itemModel.isExternallyOwned = it[i].isExternallyOwned ?: false + itemModel.callDirection = callRecord.callDirection + var dateAndDurationString = SimpleDateFormat("dd/MM/yyyy hh:mm a").format(callRecord.startTime) + dateAndDurationString += " (" + formatCallDurationTime(callRecord.duration * 1000) + ")" + itemModel.dateAndDuration = dateAndDurationString + //add in array list + itemModelList.add(itemModel) + } + adapter.itemList = itemModelList + adapter.notifyDataSetChanged() + } + } + }) + searchViewModel.searchResult.observe(viewLifecycleOwner, Observer { list -> list?.let { if (it.isEmpty()) { @@ -165,23 +195,23 @@ class SearchCommonFragment : Fragment() { } private fun updateEmptyListUI(listEmpty: Boolean) { - progress_bar.visibility = View.GONE + binding.progressBar.visibility = View.GONE if (listEmpty) { - tv_empty_data.visibility = View.VISIBLE - recycler_view.visibility = View.GONE + binding.tvEmptyData.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE } else { - tv_empty_data.visibility = View.GONE - recycler_view.visibility = View.VISIBLE + binding.tvEmptyData.visibility = View.GONE + binding.recyclerView.visibility = View.VISIBLE } } private fun updateSearchInputViewVisibility() { when (taskType) { TaskType.TaskSearchSpace -> { - search_view.visibility = View.VISIBLE + binding.searchView.visibility = View.VISIBLE } else -> { - search_view.visibility = View.GONE + binding.searchView.visibility = View.GONE } } } @@ -191,10 +221,12 @@ class SearchCommonFragment : Fragment() { lateinit var name: String lateinit var callerId: String var ongoing = false + var isExternallyOwned = false + var dateAndDuration = "" + var callDirection = CallHistoryRecord.CallDirection.UNDEFINED } - class CustomAdapter() : - RecyclerView.Adapter() { + class CustomAdapter() : RecyclerView.Adapter() { var itemList: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, i: Int): ViewHolder { @@ -223,6 +255,23 @@ class SearchCommonFragment : Fragment() { } else { binding.ongoing.visibility = View.GONE } + + if (itemModel.callDirection == CallHistoryRecord.CallDirection.OUTGOING) { + binding.callDirection.visibility = View.VISIBLE + binding.callDirection.setImageResource(R.drawable.ic_call_outgoing) + } else if (itemModel.callDirection == CallHistoryRecord.CallDirection.INCOMING) { + binding.callDirection.visibility = View.VISIBLE + binding.callDirection.setImageResource(R.drawable.ic_call_incoming) + } else { + binding.callDirection.visibility = View.GONE + } + + if (itemModel.dateAndDuration.isNotEmpty()) { + binding.startTimeAndDuration.visibility = View.VISIBLE + binding.startTimeAndDuration.text = itemModel.dateAndDuration + } else { + binding.startTimeAndDuration.visibility = View.GONE + } binding.executePendingBindings() } } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchRepository.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchRepository.kt index de90fe0..3c750ba 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchRepository.kt @@ -1,24 +1,17 @@ package com.ciscowebex.androidsdk.kitchensink.search import com.ciscowebex.androidsdk.Webex -import com.ciscowebex.androidsdk.kitchensink.messaging.spaces.SpaceModel import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.phone.CallHistoryRecord import com.ciscowebex.androidsdk.space.Space import io.reactivex.Observable import io.reactivex.Single class SearchRepository(private val webex: Webex) { - fun getCallHistory(): Observable?> { - val space = webex.phone.getCallHistory() - - return Observable.just( - space?.map { - SpaceModel(it.id.orEmpty(), it.title.orEmpty(), it.type, - it.isLocked, it.lastActivity, it.created, - it.teamId.orEmpty(), it.sipAddress.orEmpty()) - } ?: emptyList() - ) + fun getCallHistory(): Observable?> { + val callHistoryRecords = webex.phone.getCallHistory() + return Observable.just(callHistoryRecords ?: emptyList()) } fun search(query: String): Observable> { diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchViewModel.kt index f86a199..a22cdae 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchViewModel.kt @@ -7,6 +7,7 @@ import com.ciscowebex.androidsdk.kitchensink.BaseViewModel import com.ciscowebex.androidsdk.kitchensink.WebexRepository import com.ciscowebex.androidsdk.kitchensink.messaging.spaces.SpaceModel import com.ciscowebex.androidsdk.kitchensink.messaging.spaces.SpacesRepository +import com.ciscowebex.androidsdk.phone.CallHistoryRecord import com.ciscowebex.androidsdk.space.Space import com.ciscowebex.androidsdk.space.SpaceClient import io.reactivex.android.schedulers.AndroidSchedulers @@ -16,6 +17,9 @@ class SearchViewModel(private val searchRepo: SearchRepository, private val spac private val _spaces = MutableLiveData>() val spaces: LiveData> = _spaces + private val _callHistoryRecords = MutableLiveData>() + val callHistoryRecords: LiveData> = _callHistoryRecords + private val _searchResult = MutableLiveData>() val searchResult: LiveData> = _searchResult @@ -38,9 +42,9 @@ class SearchViewModel(private val searchRepo: SearchRepository, private val spac SearchCommonFragment.Companion.TaskType.TaskCallHistory -> { searchRepo.getCallHistory().observeOn(AndroidSchedulers.mainThread()).subscribe({ Log.d(tag, "Size of $taskType is ${it?.size?.or(0)}") - _spaces.postValue(it) + _callHistoryRecords.postValue(it) }, { - _spaces.postValue(emptyList()) + _callHistoryRecords.postValue(emptyList()) }).autoDispose() } SearchCommonFragment.Companion.TaskType.TaskSearchSpace -> { diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupActivity.kt index 6baa7ea..f342979 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupActivity.kt @@ -8,10 +8,12 @@ import android.widget.AdapterView import android.widget.Toast import androidx.databinding.DataBindingUtil import com.ciscowebex.androidsdk.kitchensink.BaseActivity +import com.ciscowebex.androidsdk.kitchensink.KitchenSinkForegroundService import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.WebexRepository import com.ciscowebex.androidsdk.kitchensink.databinding.ActivitySetupBinding import com.ciscowebex.androidsdk.kitchensink.utils.PermissionsHelper +import com.ciscowebex.androidsdk.kitchensink.utils.SharedPrefUtils import com.ciscowebex.androidsdk.phone.Phone import org.koin.android.ext.android.inject @@ -85,6 +87,17 @@ class SetupActivity: BaseActivity() { webexViewModel.setHardwareAccelerationEnabled(checked) } + enableAppBackgroundToggle.isChecked = SharedPrefUtils.isAppBackgroundRunningPreferred(this@SetupActivity) + + enableAppBackgroundToggle.setOnCheckedChangeListener { _, checked -> + SharedPrefUtils.setAppBackgroundRunningPreferred(this@SetupActivity, checked) + if(checked){ + KitchenSinkForegroundService.startForegroundService(this@SetupActivity) + }else{ + KitchenSinkForegroundService.stopForegroundService(this@SetupActivity) + } + } + streamModeRadioGroup.setOnCheckedChangeListener { _, checkedId -> when (checkedId) { R.id.composited -> { diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/Constants.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/Constants.kt index c371ac1..32188f3 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/Constants.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/Constants.kt @@ -11,6 +11,7 @@ class Constants { const val COMPOSER_TYPE = "composerType" const val COMPOSER_REPLY_PARENT_MESSAGE = "composerReplyParentMessage" const val CALL_ID = "callid" + const val PUSH_ID = "pushid" const val MESSAGE_ID = "MESSAGE_ID" const val CALENDAR_MEETING_ID = "CALENDAR_MEETING_ID" } @@ -27,6 +28,7 @@ class Constants { object Action { const val MESSAGE_ACTION = "MESSAGE_ACTION" const val WEBEX_CALL_ACTION = "WEBEX_CALL_ACTION" + const val WEBEX_CUCM_CALL_ACTION = "WEBEX_CUCM_CALL_ACTION" } object Keys { const val PushRestEncryptionKey = "PeShVmYq3s6v9yaBwE1H3McQfTjWnZr4" //256 bit AES key, use base64 encoded key to send to cucm endpoint @@ -34,6 +36,7 @@ class Constants { const val LoginType = "LoginType" const val Email = "Email" const val IsVirtualBgAdded = "IsVirtualBgAdded" + const val IsBackgroundRunningEnabled = "IsBackgroundRunningEnabled" } object DefaultMax { const val SPACE_MAX = 100 diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt index bc1cdbb..ebfac72 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt @@ -26,4 +26,16 @@ fun getEndOfDay(date: Date): Date { fun getCalInstance(): Calendar { return Calendar.getInstance(Locale.getDefault()) +} + +fun formatCallDurationTime(duration: Long): CharSequence { + val h = (duration / 3600000).toInt() + val m = (duration - h * 3600000).toInt() / 60000 + val s = (duration - h * 3600000 - m * 60000).toInt() / 1000 + val hh = if (h > 0) { + (if (h < 10) "0$h" else "$h") + ":" + } else { + "" + } + return hh + (if (m < 10) "0$m" else "$m") + ":" + if (s < 10) "0$s" else "$s" } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/SharedPrefUtils.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/SharedPrefUtils.kt index 66caea0..67595e5 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/SharedPrefUtils.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/SharedPrefUtils.kt @@ -64,4 +64,19 @@ object SharedPrefUtils { return false } + + fun setAppBackgroundRunningPreferred(context:Context, isPreferred: Boolean) { + val pref = context.getSharedPreferences(Constants.Keys.KitchenSinkSharedPref, Context.MODE_PRIVATE) + pref?.edit()?.putBoolean(Constants.Keys.IsBackgroundRunningEnabled, isPreferred)?.apply() + } + + fun isAppBackgroundRunningPreferred(context: Context) : Boolean { + val pref = context.getSharedPreferences(Constants.Keys.KitchenSinkSharedPref, Context.MODE_PRIVATE) + + pref?.let { + return pref.getBoolean(Constants.Keys.IsBackgroundRunningEnabled, false) + } + + return false + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/border_category_a.xml b/app/src/main/res/drawable/border_category_a.xml new file mode 100644 index 0000000..63a5a86 --- /dev/null +++ b/app/src/main/res/drawable/border_category_a.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/border_category_b.xml b/app/src/main/res/drawable/border_category_b.xml new file mode 100644 index 0000000..1d7f052 --- /dev/null +++ b/app/src/main/res/drawable/border_category_b.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/border_category_c.xml b/app/src/main/res/drawable/border_category_c.xml new file mode 100644 index 0000000..784fea4 --- /dev/null +++ b/app/src/main/res/drawable/border_category_c.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_call_incoming.xml b/app/src/main/res/drawable/ic_call_incoming.xml new file mode 100644 index 0000000..7a3a0c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_call_incoming.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_call_outgoing.xml b/app/src/main/res/drawable/ic_call_outgoing.xml new file mode 100644 index 0000000..62596a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_call_outgoing.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_black.xml b/app/src/main/res/drawable/ic_more_black.xml new file mode 100644 index 0000000..90b586b --- /dev/null +++ b/app/src/main/res/drawable/ic_more_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 0000000..6d3c825 --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/layout/activity_cucm_call.xml b/app/src/main/res/layout/activity_cucm_call.xml new file mode 100644 index 0000000..217d574 --- /dev/null +++ b/app/src/main/res/layout/activity_cucm_call.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_cucm_login.xml b/app/src/main/res/layout/activity_cucm_login.xml index d4d5d6f..6ee136f 100644 --- a/app/src/main/res/layout/activity_cucm_login.xml +++ b/app/src/main/res/layout/activity_cucm_login.xml @@ -32,11 +32,10 @@ android:id="@+id/uc_login_status_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="gone" android:layout_marginBottom="5dp" android:textColor="@android:color/black" android:textSize="15sp" - android:text="@string/uc_loggedIn"/> + android:text="@string/uc_login_status"/> + android:text="@string/phone_services_connection_status"/> diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index b79678c..c2e01b9 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -1,300 +1,333 @@ - - - - - - + android:padding="20dp"> + + + + - - + + + android:layout_marginTop="16dp"> - + - - - + - + - - - + android:layout_below="@+id/enableBgConnectionLayout" + android:layout_marginTop="16dp"> - + - - - - + - + - - - - + android:layout_below="@+id/enableBgLayout" + android:layout_marginTop="16dp"> - + - - - + + + - + android:layout_below="@+id/enablePhonePermissionLayout" + android:layout_marginTop="16dp"> - + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + android:text="@string/enable_hw_acceleration" /> - - - - - + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:checked="true" /> - - - + - + android:layout_below="@+id/enableHWAccelLayout" + android:layout_marginTop="16dp"> - + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + android:text="@string/multi_stream_new_approach" /> - - + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:checked="true" /> - + - + - + - + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + - + - + - + - - + + + + + + + + + + - + - + + - - + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_media_stream.xml b/app/src/main/res/layout/bottom_sheet_media_stream.xml new file mode 100644 index 0000000..2fd8fb9 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_media_stream.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_message_options.xml b/app/src/main/res/layout/bottom_sheet_message_options.xml index b338f32..677efb2 100644 --- a/app/src/main/res/layout/bottom_sheet_message_options.xml +++ b/app/src/main/res/layout/bottom_sheet_message_options.xml @@ -112,6 +112,28 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/fetchById" /> + + + + + app:layout_constraintTop_toBottomOf="@id/fetchByDateSeparator" /> \ No newline at end of file diff --git a/app/src/main/res/layout/common_fragment_item_list.xml b/app/src/main/res/layout/common_fragment_item_list.xml index 9926d5f..b69c8f8 100644 --- a/app/src/main/res/layout/common_fragment_item_list.xml +++ b/app/src/main/res/layout/common_fragment_item_list.xml @@ -26,15 +26,38 @@ android:orientation="horizontal" android:padding="12dp"> - + + android:layout_marginStart="@dimen/size_8dp" + android:orientation="vertical"> + + + + + diff --git a/app/src/main/res/layout/multistream_view.xml b/app/src/main/res/layout/multistream_view.xml index dde4355..9144863 100644 --- a/app/src/main/res/layout/multistream_view.xml +++ b/app/src/main/res/layout/multistream_view.xml @@ -9,8 +9,11 @@ android:id="@+id/frameLayout"> + android:layout_above="@id/name" /> + + + + 18sp 12dp 8dp + 4dp 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e5fde7..afbc02e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,8 @@ You are LoggedIn (Unified CM) Phone Services Connected Phone Services Failed: + UC Login Status : + Phone Services Connection Status : Mute all will not work for self. @@ -54,8 +56,8 @@ Message edited successfully UC Server Connected - Login Successful - Login Failed + UC Login Successful + UC Login Failed CUCM x86 not supported 1 @@ -379,4 +381,18 @@ Max Video Bandwidth Show Incoming Calls Incoming Calls + Pin Stream + Unpin Stream + Close Stream + Get previous messages by Date + Retry + OK + 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 + Keep App running in Background + Fetching Details ... + Caller details not found + Accept + Dismiss