From a3f9b2f78baa4d4f0065ab4403534435ab012f41 Mon Sep 17 00:00:00 2001 From: Rohit Sharma Date: Mon, 25 Oct 2021 21:07:11 +0530 Subject: [PATCH] v3.2.0 release --- app/src/main/AndroidManifest.xml | 9 + .../kitchensink/AccessTokenWebexModule.kt | 14 + .../androidsdk/kitchensink/HomeActivity.kt | 31 ++ .../androidsdk/kitchensink/KitchenSinkApp.kt | 10 +- .../androidsdk/kitchensink/WebexRepository.kt | 60 ++++ .../androidsdk/kitchensink/WebexViewModel.kt | 96 +++++- .../auth/AccessTokenLoginActivity.kt | 89 +++++ .../kitchensink/auth/JWTLoginActivity.kt | 8 +- .../kitchensink/auth/LoginActivity.kt | 21 +- .../kitchensink/auth/LoginRepository.kt | 14 + .../kitchensink/auth/LoginViewModel.kt | 13 + .../calling/CallBottomSheetFragment.kt | 13 +- .../calling/CallControlsFragment.kt | 141 +++++++- .../calling/CallObserverInterface.kt | 1 + .../CalendarMeetingJoinActionBottomSheet.kt | 55 +++ .../CalendarMeetingListFragment.kt | 233 +++++++++++++ .../calendarMeeting/CalendarMeetingModel.kt | 16 + .../calendarMeeting/CalendarMeetingsModule.kt | 12 + .../CalendarMeetingsRepository.kt | 34 ++ .../CalendarMeetingsViewModel.kt | 66 ++++ .../details/CalendarMeetingDetailsActivity.kt | 99 ++++++ .../CalendarMeetingDetailsViewModel.kt | 24 ++ .../spaces/detail/SpaceDetailActivity.kt | 5 +- .../kitchensink/search/SearchActivity.kt | 4 +- .../kitchensink/search/SearchViewModel.kt | 15 +- .../BackgroundOptionsBottomSheetFragment.kt | 320 ++++++++++++++++++ .../kitchensink/setup/SetupActivity.kt | 72 +--- .../kitchensink/setup/SetupCameraActivity.kt | 208 ++++++++++++ .../androidsdk/kitchensink/utils/Constants.kt | 2 + .../androidsdk/kitchensink/utils/DateUtils.kt | 29 ++ .../androidsdk/kitchensink/utils/FileUtils.kt | 27 ++ .../utils/HorizontalFlipTransformation.kt | 29 -- .../kitchensink/utils/SharedPrefUtils.kt | 15 + .../utils/extensions/ViewExtension.kt | 6 +- app/src/main/res/drawable/border.xml | 7 + .../res/drawable/ic_baseline_delete_24.xml | 5 + app/src/main/res/drawable/ic_filter.xml | 10 + .../activity_calendar_meeting_details.xml | 190 +++++++++++ .../res/layout/activity_camera_config.xml | 110 ++++++ app/src/main/res/layout/activity_login.xml | 8 + .../res/layout/activity_login_with_token.xml | 3 +- app/src/main/res/layout/activity_setup.xml | 97 +++--- ...om_sheet_calendar_meeting_join_options.xml | 110 ++++++ .../res/layout/bottom_sheet_call_options.xml | 48 ++- ...ttom_sheet_virtual_background_item_add.xml | 25 ++ ...tom_sheet_virtual_background_item_blur.xml | 25 ++ ...om_sheet_virtual_background_item_image.xml | 30 ++ ...tom_sheet_virtual_background_item_none.xml | 25 ++ .../bottom_sheet_virtual_backgrounds.xml | 24 ++ .../main/res/layout/fragment_meeting_list.xml | 96 ++++++ .../layout/list_item_calendar_meetings.xml | 60 ++++ .../res/layout/list_item_meeting_invitee.xml | 85 +++++ app/src/main/res/raw/virtual_bg.jpg | Bin 0 -> 104674 bytes app/src/main/res/values/dimens.xml | 12 +- app/src/main/res/values/integers.xml | 2 + app/src/main/res/values/strings.xml | 35 +- 56 files changed, 2608 insertions(+), 190 deletions(-) create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/AccessTokenWebexModule.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/AccessTokenLoginActivity.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingJoinActionBottomSheet.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingListFragment.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingModel.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsModule.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsRepository.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsViewModel.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsActivity.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsViewModel.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/BackgroundOptionsBottomSheetFragment.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupCameraActivity.kt create mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt delete mode 100644 app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/HorizontalFlipTransformation.kt create mode 100644 app/src/main/res/drawable/border.xml create mode 100644 app/src/main/res/drawable/ic_baseline_delete_24.xml create mode 100644 app/src/main/res/drawable/ic_filter.xml create mode 100644 app/src/main/res/layout/activity_calendar_meeting_details.xml create mode 100644 app/src/main/res/layout/activity_camera_config.xml create mode 100644 app/src/main/res/layout/bottom_sheet_calendar_meeting_join_options.xml create mode 100644 app/src/main/res/layout/bottom_sheet_virtual_background_item_add.xml create mode 100644 app/src/main/res/layout/bottom_sheet_virtual_background_item_blur.xml create mode 100644 app/src/main/res/layout/bottom_sheet_virtual_background_item_image.xml create mode 100644 app/src/main/res/layout/bottom_sheet_virtual_background_item_none.xml create mode 100644 app/src/main/res/layout/bottom_sheet_virtual_backgrounds.xml create mode 100644 app/src/main/res/layout/fragment_meeting_list.xml create mode 100644 app/src/main/res/layout/list_item_calendar_meetings.xml create mode 100644 app/src/main/res/layout/list_item_meeting_invitee.xml create mode 100644 app/src/main/res/raw/virtual_bg.jpg diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ea0c40..1456882 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,6 +85,9 @@ + @@ -134,6 +137,9 @@ + @@ -156,6 +162,9 @@ android:name=".webhooks.WebhooksActivity" android:screenOrientation="portrait" /> + + diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/AccessTokenWebexModule.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/AccessTokenWebexModule.kt new file mode 100644 index 0000000..7b32dbf --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/AccessTokenWebexModule.kt @@ -0,0 +1,14 @@ +package com.ciscowebex.androidsdk.kitchensink + +import com.ciscowebex.androidsdk.Webex +import com.ciscowebex.androidsdk.auth.JWTAuthenticator +import com.ciscowebex.androidsdk.auth.TokenAuthenticator +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +val AccessTokenWebexModule = module { + + factory { + Webex(androidApplication(), TokenAuthenticator()) + } +} \ 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 d751b16..396e72e 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/HomeActivity.kt @@ -7,7 +7,9 @@ import android.util.Log import android.view.View import androidx.databinding.DataBindingUtil import androidx.lifecycle.Observer +import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.auth.OAuthWebViewAuthenticator +import com.ciscowebex.androidsdk.auth.TokenAuthenticator import com.ciscowebex.androidsdk.kitchensink.auth.LoginActivity import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityHomeBinding import com.ciscowebex.androidsdk.kitchensink.messaging.MessagingActivity @@ -24,6 +26,9 @@ import com.ciscowebex.androidsdk.kitchensink.calling.CallActivity import com.ciscowebex.androidsdk.kitchensink.extras.ExtrasActivity import com.ciscowebex.androidsdk.kitchensink.search.SearchActivity import com.ciscowebex.androidsdk.kitchensink.setup.SetupActivity +import com.ciscowebex.androidsdk.kitchensink.utils.FileUtils +import com.ciscowebex.androidsdk.kitchensink.utils.SharedPrefUtils +import com.ciscowebex.androidsdk.message.LocalFile import com.ciscowebex.androidsdk.phone.Phone import org.koin.android.ext.android.inject @@ -51,6 +56,10 @@ class HomeActivity : BaseActivity() { is OAuthWebViewAuthenticator -> { saveLoginTypePref(this, LoginActivity.LoginType.OAuth) } + is TokenAuthenticator -> { + saveLoginTypePref(this, LoginActivity.LoginType.AccessToken) + webexViewModel.setOnTokenExpiredListener() + } else -> { saveLoginTypePref(this, LoginActivity.LoginType.JWT) } @@ -156,6 +165,7 @@ class HomeActivity : BaseActivity() { webexViewModel.setSpaceObserver() webexViewModel.setMembershipObserver() webexViewModel.setMessageObserver() + webexViewModel.setCalendarMeetingObserver() } override fun onBackPressed() { @@ -188,6 +198,7 @@ class HomeActivity : BaseActivity() { super.onResume() updateUCData() webexViewModel.setGlobalIncomingListener() + addVirtualBackground() } private fun updateUCData() { @@ -213,4 +224,24 @@ class HomeActivity : BaseActivity() { } } } + + private fun addVirtualBackground() { + if (SharedPrefUtils.isVirtualBgAdded(this)) { + Log.d(tag, "Virtual Bg is already added") + } else { + + val thumbnailFile = FileUtils.getFileFromResource(this, "nature-thumb") + val file = FileUtils.getFileFromResource(this, "nature") + val thumbnail = LocalFile.Thumbnail(thumbnailFile, null, + resources.getInteger(R.integer.virtual_bg_thumbnail_width), + resources.getInteger(R.integer.virtual_bg_thumbnail_height)) + + val localFile = LocalFile(file, null, thumbnail, null) + webexViewModel.addVirtualBackground(localFile, CompletionHandler { + if (it.isSuccessful && it.data != null) { + SharedPrefUtils.setVirtualBgAdded(this, true) + } + }) + } + } } \ No newline at end of file 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 d5bc594..b59ea04 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/KitchenSinkApp.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ProcessLifecycleOwner import com.ciscowebex.androidsdk.kitchensink.auth.LoginActivity import com.ciscowebex.androidsdk.kitchensink.auth.loginModule import com.ciscowebex.androidsdk.kitchensink.calling.callModule +import com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.calendarMeetingsModule import com.ciscowebex.androidsdk.kitchensink.extras.extrasModule import com.ciscowebex.androidsdk.kitchensink.messaging.messagingModule import com.ciscowebex.androidsdk.kitchensink.messaging.search.searchPeopleModule @@ -72,15 +73,18 @@ class KitchenSinkApp : Application(), LifecycleObserver { fun loadKoinModules(type: LoginActivity.LoginType) { when (type) { LoginActivity.LoginType.JWT -> { - loadKoinModules(listOf(mainAppModule, webexModule, loginModule, JWTWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule)) + loadKoinModules(listOf(mainAppModule, webexModule, loginModule, JWTWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) + } + LoginActivity.LoginType.AccessToken -> { + loadKoinModules(listOf(mainAppModule, webexModule, loginModule, AccessTokenWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) } else -> { - loadKoinModules(listOf(mainAppModule, webexModule, loginModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule)) + loadKoinModules(listOf(mainAppModule, webexModule, loginModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) } } } fun unloadKoinModules() { - unloadKoinModules(listOf(mainAppModule, webexModule, loginModule, JWTWebexModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule)) + unloadKoinModules(listOf(mainAppModule, webexModule, loginModule, JWTWebexModule, AccessTokenWebexModule, OAuthWebexModule, searchModule, callModule, messagingModule, personModule, searchPeopleModule, webhooksModule, extrasModule, calendarMeetingsModule)) } } \ 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 e7ed0af..5c7e9c4 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexRepository.kt @@ -11,8 +11,10 @@ import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.auth.PhoneServiceRegistrationFailureReason import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeetingObserver import com.ciscowebex.androidsdk.membership.Membership import com.ciscowebex.androidsdk.membership.MembershipObserver +import com.ciscowebex.androidsdk.message.LocalFile import com.ciscowebex.androidsdk.message.MessageObserver import com.ciscowebex.androidsdk.phone.CallMembership import com.ciscowebex.androidsdk.phone.CallObserver @@ -20,6 +22,7 @@ import com.ciscowebex.androidsdk.phone.NotificationCallType import com.ciscowebex.androidsdk.phone.Call import com.ciscowebex.androidsdk.phone.MediaOption import com.ciscowebex.androidsdk.phone.Phone +import com.ciscowebex.androidsdk.phone.VirtualBackground import com.ciscowebex.androidsdk.space.SpaceObserver class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { @@ -79,6 +82,12 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { MeetingPinOrPasswordRequired } + enum class CalendarMeetingEvent { + Created, + Updated, + Deleted + } + data class CallLiveData(val event: CallEvent, val call: Call? = null, val sharingLabel: String? = null, @@ -99,6 +108,7 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { var enableBgStreamtoggle = true var enableBgConnectiontoggle = true var enablePhoneStatePermission = true + var enableHWAcceltoggle = true var logFilter = LogLevel.ALL.name var isConsoleLoggerEnabled = true var callCapability: CallCap = CallCap.Audio_Video @@ -124,6 +134,7 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { var _spaceEventLiveData: MutableLiveData>? = null var _membershipEventLiveData: MutableLiveData>? = null var _messageEventLiveData: MutableLiveData>? = null + var _calendarMeetingEventLiveData: MutableLiveData>? = null init { webex.delegate = this @@ -223,6 +234,31 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { }) } + fun setCalendarMeetingObserver() { + webex.calendarMeetings.setObserver(object : CalendarMeetingObserver + { + override fun onEvent(event: CalendarMeetingObserver.CalendarMeetingEvent) { + Log.d(tag, "onCalendarMeetingEvent: $event") + when (event) { + is CalendarMeetingObserver.CalendarMeetingAdded -> { + _calendarMeetingEventLiveData?.postValue(Pair(CalendarMeetingEvent.Created, event.getCalendarMeeting())) + } + is CalendarMeetingObserver.CalendarMeetingUpdated -> { + _calendarMeetingEventLiveData?.postValue(Pair(CalendarMeetingEvent.Updated, event.getCalendarMeeting())) + } + is CalendarMeetingObserver.CalendarMeetingRemoved -> { + _calendarMeetingEventLiveData?.postValue(Pair(CalendarMeetingEvent.Deleted, event.getCalendarMeetingId())) + } + } + } + }) + } + + fun removeCalendarMeetingObserver() { + _calendarMeetingEventLiveData = null + webex.calendarMeetings.setObserver(null) + } + fun setIncomingListener() { Log.d(tag, "setIncomingListener") if (webex.phone.getIncomingCallListener() != null) { @@ -264,6 +300,30 @@ class WebexRepository(val webex: Webex) : WebexUCLoginDelegate { webex.messages.list(spaceId, null, 10000, null, handler) } + fun getVirtualBackgrounds(handler: CompletionHandler> ) { + webex.phone.fetchVirtualBackgrounds(handler) + } + + fun addVirtualBackground(imgFile: LocalFile, handler: CompletionHandler) { + webex.phone.addVirtualBackground(imgFile, handler) + } + + fun applyVirtualBackground(background: VirtualBackground, mode: Phone.VirtualBackgroundMode, handler: CompletionHandler) { + webex.phone.applyVirtualBackground(background, mode, handler) + } + + fun removeVirtualBackground(background: VirtualBackground, handler: CompletionHandler) { + webex.phone.removeVirtualBackground(background, handler) + } + + fun setMaxVirtualBackgrounds(limit: Int) { + webex.phone.setMaxVirtualBackgroundItems(limit) + } + + fun getMaxVirtualBackgrounds(): Int { + return webex.phone.getMaxVirtualBackgroundItems() + } + // Callbacks override fun showUCSSOLoginView(ssoUrl: String) { _cucmLiveData?.postValue(Pair(CucmEvent.ShowSSOLogin, ssoUrl)) 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 3b309e8..d8a9c4f 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/WebexViewModel.kt @@ -23,11 +23,14 @@ import com.google.firebase.messaging.FirebaseMessaging import org.json.JSONObject import com.ciscowebex.androidsdk.phone.CallAssociationType import com.ciscowebex.androidsdk.auth.PhoneServiceRegistrationFailureReason +import com.ciscowebex.androidsdk.auth.TokenAuthenticator import com.ciscowebex.androidsdk.auth.UCLoginServerConnectionStatus import com.ciscowebex.androidsdk.kitchensink.calling.CallObserverInterface import com.ciscowebex.androidsdk.kitchensink.utils.CallObjectStorage +import com.ciscowebex.androidsdk.message.LocalFile import com.ciscowebex.androidsdk.phone.AdvancedSetting import com.ciscowebex.androidsdk.phone.AuxStream +import com.ciscowebex.androidsdk.phone.VirtualBackground class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseViewModel() { @@ -62,11 +65,19 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi private val _tokenLiveData = MutableLiveData>() val tokenLiveData: LiveData> = _tokenLiveData + private val _virtualBackground = MutableLiveData>() + val virtualBackground: LiveData> = _virtualBackground + + private val _virtualBgError = MutableLiveData() + val virtualBgError: LiveData = _virtualBgError + var selfPersonId: String? = null var compositedLayoutState = MediaOption.CompositedVideoLayout.NOT_SUPPORTED var callObserverInterface: CallObserverInterface? = null + var isVideoViewsSwapped: Boolean = true + var callCapability: WebexRepository.CallCap get() = repository.callCapability set(value) { @@ -181,6 +192,12 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi repository.enablePhoneStatePermission = value } + var enableHWAcceltoggle: Boolean + get() = repository.enableHWAcceltoggle + set(value) { + repository.enableHWAcceltoggle = value + } + var logFilter: String get() = repository.logFilter set(value) { @@ -237,10 +254,15 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi repository.setMessageObserver() } + fun setCalendarMeetingObserver() { + repository.setCalendarMeetingObserver() + } + fun setIncomingListener() { webex.phone.setIncomingCallListener(object : Phone.IncomingCallListener { override fun onIncomingCall(call: Call?) { call?.let { + Log.d(tag, "setIncomingCallListener Call object : ${it.getCallId()}") CallObjectStorage.addCallObject(it) _incomingListenerLiveData.postValue(it) setCallObserver(it) @@ -275,7 +297,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi fun dial(input: String, option: MediaOption) { webex.phone.dial(input, option, CompletionHandler { result -> - Log.d(tag, "Omnius: onCallEvent CallStateChanged") + Log.d(tag, "dial isSuccessful: ${result.isSuccessful}") if (result.isSuccessful) { result.data?.let { _call -> CallObjectStorage.addCallObject(_call) @@ -347,6 +369,10 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi override fun onScheduleChanged(call: Call?) { callObserverInterface?.onScheduleChanged(call) } + + override fun onCpuHitThreshold() { + callObserverInterface?.onCpuHitThreshold() + } }) } @@ -588,7 +614,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi fun switchAudioMode(mode: Call.AudioOutputMode) { getCall(currentCallId.orEmpty())?.switchAudioOutput(mode) } - + fun enableAudioBNR(value: Boolean) { webex.phone.enableAudioBNR(value) } @@ -704,7 +730,7 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi fun letIn(callId: String, callMembership: CallMembership) { getCall(callId)?.letIn(callMembership) } - + fun setVideoStreamMode(mode: Phone.VideoStreamMode) { webex.phone.setVideoStreamMode(mode) } @@ -777,4 +803,68 @@ class WebexViewModel(val webex: Webex, val repository: WebexRepository) : BaseVi fun getServiceUrl(type: Phone.ServiceUrlType): String? { return webex.phone.getServiceUrl(type) } + + fun setOnTokenExpiredListener() { + webex.authenticator?.let { + if (it is TokenAuthenticator) { + it.setOnTokenExpiredListener(CompletionHandler { + Log.d(tag, "KS setOnTokenExpiredListener") + _signOutListenerLiveData.postValue(it.isSuccessful) + }) + } + } + } + + fun isVirtualBackgroundSupported() = webex.phone.isVirtualBackgroundSupported() + + fun fetchVirtualBackgrounds() { + repository.getVirtualBackgrounds(CompletionHandler { + if (it.isSuccessful) + _virtualBackground.postValue(it.data) + else + _virtualBgError.postValue(it.error?.errorMessage) + }) + } + + fun addVirtualBackground(imgFile: LocalFile) { + repository.addVirtualBackground(imgFile, CompletionHandler { + if (it.isSuccessful) + fetchVirtualBackgrounds() + else + _virtualBgError.postValue(it.error?.errorMessage) + }) + } + + fun addVirtualBackground(imgFile: LocalFile, handler: CompletionHandler) { + repository.addVirtualBackground(imgFile, handler) + } + + fun applyVirtualBackground(background: VirtualBackground, mode: Phone.VirtualBackgroundMode) { + repository.applyVirtualBackground(background, mode, CompletionHandler { + if (it.isSuccessful && it.data == true) + Log.d(tag, "virtual background applied") + else + _virtualBgError.postValue(it.error?.errorMessage) + }) + } + + fun removeVirtualBackground(background: VirtualBackground) { + repository.removeVirtualBackground(background, CompletionHandler { + if (it.isSuccessful && it.data == true) { + Log.d(tag, "virtual background removed") + fetchVirtualBackgrounds() + } + else { + _virtualBgError.postValue(it.error?.errorMessage) + } + }) + } + + fun setMaxVirtualBackgrounds(limit: Int) { + repository.setMaxVirtualBackgrounds(limit) + } + + fun getMaxVirtualBackgrounds(): Int { + return repository.getMaxVirtualBackgrounds() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/AccessTokenLoginActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/AccessTokenLoginActivity.kt new file mode 100644 index 0000000..8c6af31 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/AccessTokenLoginActivity.kt @@ -0,0 +1,89 @@ +package com.ciscowebex.androidsdk.kitchensink.auth + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import com.ciscowebex.androidsdk.kitchensink.HomeActivity +import com.ciscowebex.androidsdk.kitchensink.KitchenSinkApp +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityLoginWithTokenBinding +import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage +import org.koin.android.viewmodel.ext.android.viewModel + +class AccessTokenLoginActivity: AppCompatActivity() { + + lateinit var binding: ActivityLoginWithTokenBinding + private val loginViewModel: LoginViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + DataBindingUtil.setContentView(this, R.layout.activity_login_with_token) + .also { binding = it } + .apply { + title.text = getString(R.string.login_access_token) + progressLayout.visibility = View.VISIBLE + loginButton.setOnClickListener { + binding.loginFailedTextView.visibility = View.GONE + if (tokenText.text.isEmpty()) { + showDialogWithMessage(this@AccessTokenLoginActivity, R.string.error_occurred, resources.getString( + R.string.login_token_empty_error)) + } + else { + binding.loginButton.visibility = View.GONE + progressLayout.visibility = View.VISIBLE + val token = tokenText.text.toString() + loginViewModel.loginWithAccessToken(token, null) + } + } + + loginViewModel.isAuthorized.observe(this@AccessTokenLoginActivity, Observer { isAuthorized -> + progressLayout.visibility = View.GONE + isAuthorized?.let { + if (it) { + onLoggedIn() + } else { + onLoginFailed() + } + } + }) + + loginViewModel.isAuthorizedCached.observe(this@AccessTokenLoginActivity, Observer { isAuthorizedCached -> + progressLayout.visibility = View.GONE + isAuthorizedCached?.let { + if (it) { + onLoggedIn() + } else { + tokenText.visibility = View.VISIBLE + loginButton.visibility = View.VISIBLE + loginFailedTextView.visibility = View.GONE + } + } + }) + + loginViewModel.errorData.observe(this@AccessTokenLoginActivity, Observer { errorMessage -> + progressLayout.visibility = View.GONE + onLoginFailed(errorMessage) + }) + + loginViewModel.initialize() + } + } + + override fun onBackPressed() { + (application as KitchenSinkApp).closeApplication() + } + + private fun onLoggedIn() { + startActivity(Intent(this, HomeActivity::class.java)) + finish() + } + + private fun onLoginFailed(failureMessage: String = getString(R.string.auth_token_login_failed)) { + binding.loginButton.visibility = View.VISIBLE + binding.loginFailedTextView.visibility = View.VISIBLE + binding.loginFailedTextView.text = failureMessage + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/JWTLoginActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/JWTLoginActivity.kt index 7476de1..d3ce856 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/JWTLoginActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/JWTLoginActivity.kt @@ -27,13 +27,13 @@ class JWTLoginActivity : AppCompatActivity() { progressLayout.visibility = View.VISIBLE loginButton.setOnClickListener { binding.loginFailedTextView.visibility = View.GONE - if (jwtTokenText.text.isEmpty()) { - showDialogWithMessage(this@JWTLoginActivity, R.string.error_occurred, resources.getString(R.string.jwt_login_token_empty_error)) + if (tokenText.text.isEmpty()) { + showDialogWithMessage(this@JWTLoginActivity, R.string.error_occurred, resources.getString(R.string.login_token_empty_error)) } else { binding.loginButton.visibility = View.GONE progressLayout.visibility = View.VISIBLE - val token = jwtTokenText.text.toString() + val token = tokenText.text.toString() loginViewModel.loginWithJWT(token) } } @@ -55,7 +55,7 @@ class JWTLoginActivity : AppCompatActivity() { if (it) { onLoggedIn() } else { - jwtTokenText.visibility = View.VISIBLE + tokenText.visibility = View.VISIBLE loginButton.visibility = View.VISIBLE loginFailedTextView.visibility = View.GONE } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginActivity.kt index 4b6e88a..4771289 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginActivity.kt @@ -19,7 +19,8 @@ class LoginActivity : AppCompatActivity() { enum class LoginType(var value: String) { OAuth("OAuth"), - JWT("JWT") + JWT("JWT"), + AccessToken("AccessToken") } private var loginTypeCalled = LoginType.OAuth @@ -39,6 +40,12 @@ class LoginActivity : AppCompatActivity() { startActivity(Intent(this@LoginActivity, JWTLoginActivity::class.java)) finish() } + LoginType.AccessToken.value -> { + loginTypeCalled = LoginType.AccessToken + (application as KitchenSinkApp).loadKoinModules(loginTypeCalled) + startActivity(Intent(this@LoginActivity, AccessTokenLoginActivity::class.java)) + finish() + } LoginType.OAuth.value -> { loginTypeCalled = LoginType.OAuth (application as KitchenSinkApp).loadKoinModules(loginTypeCalled) @@ -55,6 +62,9 @@ class LoginActivity : AppCompatActivity() { buttonClicked(LoginType.OAuth) } + btnAccessLogin.setOnClickListener { + buttonClicked(LoginType.AccessToken) + } } } @@ -69,6 +79,9 @@ class LoginActivity : AppCompatActivity() { LoginType.OAuth -> { showEmailDialog(type) } + LoginType.AccessToken -> { + startAccessTokenActivity() + } } } @@ -96,6 +109,12 @@ class LoginActivity : AppCompatActivity() { finish() } + private fun startAccessTokenActivity() { + (application as KitchenSinkApp).loadKoinModules(loginTypeCalled) + startActivity(Intent(this@LoginActivity, AccessTokenLoginActivity::class.java)) + finish() + } + private fun showEmailDialog(type: LoginType) { showDialogForInputEmail(this, getString(R.string.enter_user_email_address), onPositiveButtonClick = { dialog: DialogInterface, email: String -> when (type) { diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginRepository.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginRepository.kt index aaff8f7..03308d9 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginRepository.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginRepository.kt @@ -6,6 +6,7 @@ import com.ciscowebex.androidsdk.Webex import com.ciscowebex.androidsdk.auth.JWTAuthenticator import com.ciscowebex.androidsdk.auth.OAuthWebViewAuthenticator import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.auth.TokenAuthenticator import io.reactivex.Observable import io.reactivex.Single @@ -28,6 +29,7 @@ class LoginRepository() { webex.initialize(CompletionHandler { result -> Log.d("LoginRepository:initialize ", "isAuthorized : ${webex.authenticator?.isAuthorized()}") if (result.error != null) { + Log.d("LoginRepository:initialize ", "errorCode : ${result.error?.errorCode}, errorMessage : ${result.error?.errorMessage}") emitter.onError(Throwable(result.error?.errorMessage)) } else { emitter.onSuccess(result.isSuccessful) @@ -49,4 +51,16 @@ class LoginRepository() { }.toObservable() } + fun loginWithAccessToken(token: String, expiryInSeconds: Int?, tokenAuthenticator: TokenAuthenticator): Observable { + return Single.create { emitter -> + tokenAuthenticator.authorize(token, expiryInSeconds, CompletionHandler { result -> + Log.d("LoginRepository:loginWithAccessToken ", "isAuthorized : ${tokenAuthenticator.isAuthorized()}") + if (result.error != null) { + emitter.onError(Throwable(result.error?.errorMessage)) + } else { + emitter.onSuccess(result.isSuccessful) + } + }) + }.toObservable() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginViewModel.kt index 9afded7..52180a3 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginViewModel.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/auth/LoginViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import com.ciscowebex.androidsdk.Webex import com.ciscowebex.androidsdk.auth.JWTAuthenticator import com.ciscowebex.androidsdk.auth.OAuthWebViewAuthenticator +import com.ciscowebex.androidsdk.auth.TokenAuthenticator import com.ciscowebex.androidsdk.kitchensink.BaseViewModel import io.reactivex.android.schedulers.AndroidSchedulers @@ -51,4 +52,16 @@ class LoginViewModel(private val webex: Webex, private val loginRepository: Logi } } + fun loginWithAccessToken(token: String, expiryInSeconds: Int?) { + val tokenAuthenticator = webex.authenticator as? TokenAuthenticator + tokenAuthenticator?.let { auth -> + loginRepository.loginWithAccessToken(token, expiryInSeconds, auth).observeOn(AndroidSchedulers.mainThread()).subscribe({ + _isAuthorized.postValue(it) + }, { + _errorData.postValue(it.message) + }).autoDispose() + } ?: run { + _isAuthorized.postValue(false) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallBottomSheetFragment.kt index 3d1e0f7..77b4adf 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 @@ -15,7 +15,9 @@ class CallBottomSheetFragment(val receivingVideoClickListener: (Call?) -> Unit, val receivingAudioClickListener: (Call?) -> Unit, val receivingSharingClickListener: (Call?) -> Unit, val scalingModeClickListener: (Call?) -> Unit, - val compositeStreamLayoutClickListener: (Call?) -> Unit): BottomSheetDialogFragment() { + val virtualBackgroundOptionsClickListener: (Call?) -> Unit, + val compositeStreamLayoutClickListener: (Call?) -> Unit, + val swapVideoClickListener: (Call?) -> Unit): BottomSheetDialogFragment() { companion object { val TAG = "MessageActionBottomSheetFragment" } @@ -131,6 +133,15 @@ class CallBottomSheetFragment(val receivingVideoClickListener: (Call?) -> Unit, compositeStreamLayoutClickListener(call) } + swapVideo.setOnClickListener { + dismiss() + swapVideoClickListener(call) + } + + bgOptionsBtn.setOnClickListener { + dismiss() + virtualBackgroundOptionsClickListener(call) + } cancel.setOnClickListener { dismiss() } }.root } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallControlsFragment.kt index 99bb2a2..f9d5537 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 @@ -1,5 +1,6 @@ package com.ciscowebex.androidsdk.kitchensink.calling +import android.annotation.SuppressLint import android.app.Activity import android.app.AlertDialog import android.app.NotificationChannel @@ -50,11 +51,19 @@ import com.ciscowebex.androidsdk.phone.MediaRenderView import com.ciscowebex.androidsdk.phone.MultiStreamObserver import com.ciscowebex.androidsdk.phone.AuxStream import org.koin.android.ext.android.inject -import android.widget.EditText import android.widget.Toast +import com.ciscowebex.androidsdk.WebexError +import com.ciscowebex.androidsdk.CompletionHandler import com.ciscowebex.androidsdk.kitchensink.databinding.DialogCreateSpaceBinding import com.ciscowebex.androidsdk.kitchensink.databinding.DialogEnterMeetingPinBinding +import com.ciscowebex.androidsdk.kitchensink.setup.BackgroundOptionsBottomSheetFragment +import com.ciscowebex.androidsdk.kitchensink.utils.extensions.toast import com.ciscowebex.androidsdk.kitchensink.utils.showDialogWithMessage +import com.ciscowebex.androidsdk.message.LocalFile +import com.ciscowebex.androidsdk.phone.VirtualBackground +import com.ciscowebex.androidsdk.utils.internal.MimeUtils +import java.io.File +import java.lang.Integer.getInteger import java.util.Date @@ -75,6 +84,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface private lateinit var incomingInfoAdapter: IncomingInfoAdapter private val mAuxStreamViewMap: HashMap = HashMap() private var callerId: String = "" + var bottomSheetFragment: BackgroundOptionsBottomSheetFragment? = null enum class ShareButtonState { OFF, @@ -183,6 +193,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface var callEnded = false var localClose = false + var failedError: WebexError? = null event?.let { _event -> val _call = _event.getCall() when (_event) { @@ -214,6 +225,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } is CallObserver.CallErrorEvent -> { Log.d(TAG, "CallObserver CallErrorEvent") + failedError = _event.getError() callFailed = true } is CallObserver.CallEnded -> { @@ -226,7 +238,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface when { callFailed -> { - onCallFailed(call?.getCallId().orEmpty()) + onCallFailed(call?.getCallId().orEmpty(), failedError) } callEnded -> { onCallTerminated(call?.getCallId().orEmpty()) @@ -373,6 +385,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface schedulesChanged(call) } + override fun onCpuHitThreshold() { + Log.d(TAG, "CallObserver onCpuHitThreshold") + } + + @SuppressLint("NotifyDataSetChanged") private fun observerCallLiveData() { personViewModel.person.observe(viewLifecycleOwner, Observer { person -> @@ -487,6 +504,98 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } } }) + + webexViewModel.virtualBgError.observe(viewLifecycleOwner, Observer { error -> + Log.d(tag, error) + requireContext().toast(error) + }) + + webexViewModel.virtualBackground.observe(viewLifecycleOwner, Observer { + val emptyBackground = VirtualBackground() + + if (bottomSheetFragment == null) { + bottomSheetFragment = + BackgroundOptionsBottomSheetFragment(onBackgroundChanged = { virtualBackground -> + if (!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, getString(R.string.virtual_bg_not_supported)) + Toast.makeText( + requireContext(), + getString(R.string.virtual_bg_not_supported), + Toast.LENGTH_SHORT + ) + .show() + return@BackgroundOptionsBottomSheetFragment + } + + webexViewModel.applyVirtualBackground( + virtualBackground, + Phone.VirtualBackgroundMode.CALL + ) + }, + onBackgroundRemoved = { virtualBackground -> + webexViewModel.removeVirtualBackground(virtualBackground) + }, + onNewBackgroundAdded = { file -> + if (!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, getString(R.string.virtual_bg_not_supported)) + Toast.makeText( + requireContext(), + getString(R.string.virtual_bg_not_supported), + Toast.LENGTH_SHORT + ) + .show() + return@BackgroundOptionsBottomSheetFragment + } + + val localFile = processAttachmentFile(file) + webexViewModel.addVirtualBackground(localFile) + }, + onBottomSheetDimissed = { + bottomSheetFragment = null + }) + + bottomSheetFragment?.show( + childFragmentManager, + BackgroundOptionsBottomSheetFragment::class.java.name + ) + } + + bottomSheetFragment?.backgrounds?.clear() + bottomSheetFragment?.backgrounds?.addAll(it) + bottomSheetFragment?.backgrounds?.add(emptyBackground) + bottomSheetFragment?.adapter?.notifyDataSetChanged() + }) + } + + private fun handleOnBackgroundChanged(virtualBackground: VirtualBackground) { + if(!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, "virtual background is not supported") + requireContext().toast(getString(R.string.virtual_bg_not_supported)) + return + } + + webexViewModel.applyVirtualBackground(virtualBackground, Phone.VirtualBackgroundMode.CALL) + } + + private fun handleOnNewBackgroundAdded(file: File) { + if(!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, "virtual background is not supported") + requireContext().toast(getString(R.string.virtual_bg_not_supported)) + return + } + + val localFile = processAttachmentFile(file) + webexViewModel.addVirtualBackground(localFile) + } + + + private fun processAttachmentFile(file: File): LocalFile { + var thumbnail: LocalFile.Thumbnail? = null + if (MimeUtils.getContentTypeByFilename(file.name) == MimeUtils.ContentType.IMAGE) { + thumbnail = LocalFile.Thumbnail(file, null, resources.getInteger(R.integer.virtual_bg_thumbnail_width), resources.getInteger(R.integer.virtual_bg_thumbnail_height)) + } + + return LocalFile(file, null, thumbnail, null) } private fun onMeetingHostPinError() { @@ -635,11 +744,11 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface webexViewModel.setAudioBNRMode(Phone.AudioBRNMode.HP) webexViewModel.setDefaultFacingMode(Phone.FacingMode.USER) - webexViewModel.setVideoMaxTxFPSSetting(5) + webexViewModel.setVideoMaxTxFPSSetting(30) webexViewModel.setVideoEnableCamera2Setting(true) webexViewModel.setVideoEnableDecoderMosaicSetting(true) - webexViewModel.setHardwareAccelerationEnabled(true) + webexViewModel.setHardwareAccelerationEnabled(webexViewModel.enableHWAcceltoggle) webexViewModel.setVideoMaxRxBandwidth(Phone.DefaultBandwidth.MAX_BANDWIDTH_720P.getValue()) webexViewModel.setVideoMaxTxBandwidth(Phone.DefaultBandwidth.MAX_BANDWIDTH_720P.getValue()) webexViewModel.setSharingMaxRxBandwidth(Phone.DefaultBandwidth.MAX_BANDWIDTH_SESSION.getValue()) @@ -671,7 +780,9 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface { call -> receivingAudioListener(call) }, { call -> receivingSharingListener(call) }, { call -> scalingModeClickListener(call) }, - { call -> compositeStreamLayoutClickListener(call) }) + { call -> virtualBackgroundOptionsClickListener(call) }, + { call -> compositeStreamLayoutClickListener(call) }, + { call -> swapVideoClickListener(call) }) callingActivity = activity?.intent?.getIntExtra(Constants.Intent.CALLING_ACTIVITY_ID, 0)!! if (callingActivity == 1) { @@ -683,6 +794,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface webexViewModel.setIncomingListener() webexViewModel.incomingListenerLiveData.observe(viewLifecycleOwner, Observer { it?.let { + Log.d(tag, "incomingListenerLiveData: ${it.getCallId()}") ringerManager.startRinger(RingerManager.RingerType.Incoming) onIncomingCall(it) } @@ -1373,7 +1485,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } } - private fun onCallFailed(callId: String) { + private fun onCallFailed(callId: String, failedError: WebexError?) { Log.d(TAG, "CallControlsFragment onCallFailed callerId: $callId") Handler(Looper.getMainLooper()).post { @@ -1385,7 +1497,7 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface callFailed = !webexViewModel.isAddedCall val callActivity = activity as CallActivity? - callActivity?.alertDialog(!webexViewModel.isAddedCall, "") + callActivity?.alertDialog(!webexViewModel.isAddedCall, failedError?.errorMessage.orEmpty()) } } @@ -1567,6 +1679,17 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface } } + private fun swapVideoClickListener(call: Call?) { + Log.d(TAG, "swapVideoClickListener") + if (webexViewModel.isVideoViewsSwapped) { + webexViewModel.setVideoRenderViews(webexViewModel.currentCallId.orEmpty(), binding.remoteView, binding.localView) + webexViewModel.isVideoViewsSwapped = false + } else { + webexViewModel.setVideoRenderViews(webexViewModel.currentCallId.orEmpty(), binding.localView, binding.remoteView) + webexViewModel.isVideoViewsSwapped = true + } + } + private fun compositeStreamLayoutClickListener(call: Call?) { Log.d(TAG, "compositeStreamLayoutClickListener getCompositedLayout: ${webexViewModel.getCompositedLayout()}") @@ -1593,6 +1716,10 @@ class CallControlsFragment : Fragment(), OnClickListener, CallObserverInterface webexViewModel.setCompositedLayout(layout) } + private fun virtualBackgroundOptionsClickListener(call: Call?) { + webexViewModel.fetchVirtualBackgrounds() + } + private fun scalingModeClickListener(call: Call?) { Log.d(TAG, "scalingModeClickListener") diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt index 67d3a6f..59b993a 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/CallObserverInterface.kt @@ -18,4 +18,5 @@ interface CallObserverInterface { fun onMediaChanged(call: Call?, event: CallObserver.MediaChangedEvent?) {} fun onCallMembershipChanged(call: Call?, event: CallObserver.CallMembershipChangedEvent?) {} fun onScheduleChanged(call: Call?) {} + fun onCpuHitThreshold() {} } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingJoinActionBottomSheet.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingJoinActionBottomSheet.kt new file mode 100644 index 0000000..1140947 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingJoinActionBottomSheet.kt @@ -0,0 +1,55 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetCalendarMeetingJoinOptionsBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class CalendarMeetingJoinActionBottomSheet( + val joinByMeetingIdClickListener: (String) -> Unit, + val joinByMeetingLinkClickListener: (String) -> Unit, + val joinByMeetingNumberClickListener: (String) -> Unit +) : BottomSheetDialogFragment() { + + private lateinit var binding: BottomSheetCalendarMeetingJoinOptionsBinding + var meeting : CalendarMeeting? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return BottomSheetCalendarMeetingJoinOptionsBinding.inflate(inflater, container, false) + .also { binding = it }.apply { + // Control joining options visibility + if (meeting?.sipUrl.isNullOrEmpty()) { + tvJoinByMeetingNumber.visibility = View.GONE + } + + if (meeting?.link.isNullOrEmpty()) { + tvJoinByMeetingLink.visibility = View.GONE + } + + tvJoinByMeetingId.setOnClickListener { + dismiss() + joinByMeetingIdClickListener(meeting?.id ?: "") + } + + tvJoinByMeetingLink.setOnClickListener { + dismiss() + joinByMeetingLinkClickListener(meeting?.link ?: "") + } + + tvJoinByMeetingNumber.setOnClickListener { + dismiss() + joinByMeetingNumberClickListener(meeting?.sipUrl ?: "") + } + + tvCancel.setOnClickListener { dismiss() } + }.root + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingListFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingListFragment.kt new file mode 100644 index 0000000..de665d0 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingListFragment.kt @@ -0,0 +1,233 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +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.calendarMeeting.details.CalendarMeetingDetailsActivity +import com.ciscowebex.androidsdk.kitchensink.databinding.FragmentMeetingListBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.ListItemCalendarMeetingsBinding +import org.koin.android.ext.android.inject +import java.util.Date + +class CalendarMeetingListFragment : Fragment() { + private lateinit var binding : FragmentMeetingListBinding + private lateinit var calendarMeetingListAdapter : CalendarMeetingListAdapter + + private val meetingsViewModel: CalendarMeetingsViewModel by inject() + + private var isFABOpen = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return FragmentMeetingListBinding.inflate(inflater, container, false).also { binding = it }.apply { + val meetingJoinOptionsDialogFragment = CalendarMeetingJoinActionBottomSheet( + {meetingId -> joinByMeetingId(meetingId)}, + {meetingLink -> joinByMeetingLink(meetingLink)}, + {sipUrl -> joinBySipUrl(sipUrl)} + ) + calendarMeetingListAdapter = CalendarMeetingListAdapter(meetingJoinOptionsDialogFragment, requireActivity().supportFragmentManager) { listItemPosition -> + context?.let { + val meetingItem = calendarMeetingListAdapter.meetings[listItemPosition] + it.startActivity(CalendarMeetingDetailsActivity.getIntent(it, meetingItem.calendarMeeting.id ?: "")) + } + } + meetingListRecyclerView.adapter = calendarMeetingListAdapter + meetingListRecyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + lifecycleOwner = this@CalendarMeetingListFragment + setUpObservers() + setFilterMeetingListeners() + }.root + } + + override fun onPause() { + super.onPause() + closeFABMenu() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + meetingsViewModel.getCalendarMeetingsList() + } + + private fun setFilterMeetingListeners() { + binding.filterMeetingsFAB.setOnClickListener { + if(!isFABOpen) showFABMenu() else closeFABMenu() + } + binding.tvToday.setOnClickListener { + meetingsViewModel.onFilterItemClick(CalendarMeetingsViewModel.FilterMeetingsBy.Today) + closeFABMenu() + } + binding.tvTomorrow.setOnClickListener { + meetingsViewModel.onFilterItemClick(CalendarMeetingsViewModel.FilterMeetingsBy.Tomorrow) + closeFABMenu() + } + binding.tvUpcomingMeetings.setOnClickListener { + meetingsViewModel.onFilterItemClick(CalendarMeetingsViewModel.FilterMeetingsBy.UpcomingMeetings) + closeFABMenu() + } + binding.tvPastMeetings.setOnClickListener { + meetingsViewModel.onFilterItemClick(CalendarMeetingsViewModel.FilterMeetingsBy.PastMeetings) + closeFABMenu() + } + binding.tvAllMeetings.setOnClickListener { + meetingsViewModel.onFilterItemClick(CalendarMeetingsViewModel.FilterMeetingsBy.AllMeetings) + closeFABMenu() + } + } + + private fun showFABMenu() { + isFABOpen = true + binding.tvUpcomingMeetings.animate().alpha(1F).duration = 250 + binding.tvUpcomingMeetings.animate().translationY(-resources.getDimension(R.dimen.filter_meetings_pos1)) + binding.tvPastMeetings.animate().alpha(1F).duration = 250 + binding.tvPastMeetings.animate().translationY(-resources.getDimension(R.dimen.filter_meetings_pos2)) + binding.tvTomorrow.animate().alpha(1F).duration = 250 + binding.tvTomorrow.animate().translationY(-resources.getDimension(R.dimen.filter_meetings_pos3)) + binding.tvToday.animate().alpha(1F).duration = 250 + binding.tvToday.animate().translationY(-resources.getDimension(R.dimen.filter_meetings_pos4)) + binding.tvAllMeetings.animate().alpha(1F).duration = 250 + binding.tvAllMeetings.animate().translationY(-resources.getDimension(R.dimen.filter_meetings_pos5)) + } + + private fun closeFABMenu() { + isFABOpen = false + binding.tvUpcomingMeetings.animate().translationY(0F) + binding.tvUpcomingMeetings.animate().alpha(0F).duration = 300 + binding.tvPastMeetings.animate().translationY(0F) + binding.tvPastMeetings.animate().alpha(0F).duration = 300 + binding.tvTomorrow.animate().translationY(0F) + binding.tvTomorrow.animate().alpha(0F).duration = 300 + binding.tvToday.animate().translationY(0F) + binding.tvToday.animate().alpha(0F).duration = 300 + binding.tvAllMeetings.animate().translationY(0F) + binding.tvAllMeetings.animate().alpha(0F).duration = 300 + } + + private fun setUpObservers() { + meetingsViewModel.meetings.observe(this@CalendarMeetingListFragment.viewLifecycleOwner, Observer { meetings -> + calendarMeetingListAdapter.meetings.clear() + calendarMeetingListAdapter.meetings.addAll(meetings.map { CalendarMeetingModel(it) }) + calendarMeetingListAdapter.notifyDataSetChanged() + }) + + meetingsViewModel.getCalendarMeetingEvent()?.observe(this@CalendarMeetingListFragment.viewLifecycleOwner, Observer { pair -> + when(pair.first) { + WebexRepository.CalendarMeetingEvent.Created -> { + val newMeeting = pair.second as CalendarMeeting + val meetingModels = calendarMeetingListAdapter.meetings + val index = meetingModels.indexOfFirst { it.calendarMeeting.startTime?.time?: 0 > newMeeting.startTime?.time?: 0 } + + if (index == -1) { + calendarMeetingListAdapter.meetings.add(CalendarMeetingModel(newMeeting)) + calendarMeetingListAdapter.notifyItemInserted(calendarMeetingListAdapter.meetings.size - 1) + } else { + calendarMeetingListAdapter.meetings.add(index, CalendarMeetingModel(newMeeting)) + calendarMeetingListAdapter.notifyItemInserted(index) + } + } + WebexRepository.CalendarMeetingEvent.Updated -> { + val meeting = pair.second as CalendarMeeting + val index = calendarMeetingListAdapter.getPositionById(meeting.id?: "") + if (!calendarMeetingListAdapter.meetings.isNullOrEmpty() && index != -1) { + calendarMeetingListAdapter.meetings[index] = CalendarMeetingModel(meeting) + calendarMeetingListAdapter.notifyItemChanged(index) + } + } + WebexRepository.CalendarMeetingEvent.Deleted -> { + val meetingId = pair.second as String + val index = calendarMeetingListAdapter.getPositionById(meetingId) + if (!calendarMeetingListAdapter.meetings.isNullOrEmpty() && index != -1) { + val meeting = calendarMeetingListAdapter.meetings[index] + calendarMeetingListAdapter.meetings.remove(meeting) + calendarMeetingListAdapter.notifyItemRemoved(index) + } + } + } + }) + } + + private fun joinByMeetingId(meetingId: String) { + context?.let { + startActivity(CallActivity.getOutgoingIntent(it, meetingId)) + } + } + + private fun joinByMeetingLink(meetingLink: String) { + context?.let { + startActivity(CallActivity.getOutgoingIntent(it, meetingLink)) + } + } + + private fun joinBySipUrl(sipUrl: String) { + context?.let { + startActivity(CallActivity.getOutgoingIntent(it, sipUrl)) + } + } + + class CalendarMeetingListAdapter( + private val meetingJoinOptionsDialogFragment: CalendarMeetingJoinActionBottomSheet, + private val supportFragmentManager: FragmentManager, + private val onListItemClicked : (listItemPosition: Int) -> Unit + ) : RecyclerView.Adapter() { + var meetings: MutableList = mutableListOf() + + fun getPositionById(meetingId: String): Int { + return meetings.indexOfFirst { it.calendarMeeting.id == meetingId } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MeetingListViewHolder { + return MeetingListViewHolder( + meetingJoinOptionsDialogFragment, + supportFragmentManager, + ListItemCalendarMeetingsBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ), + onListItemClicked + ) + } + + override fun getItemCount(): Int = meetings.size + + override fun onBindViewHolder(holder: MeetingListViewHolder, position: Int) { + holder.bind(meetings[position]) + } + } + + class MeetingListViewHolder( + private val meetingJoinOptionsDialogFragment: CalendarMeetingJoinActionBottomSheet, + private val supportFragmentManager: FragmentManager, + private val binding: ListItemCalendarMeetingsBinding, + private val onListItemClicked : (listItemPosition: Int) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.root.setOnClickListener { + onListItemClicked(adapterPosition) + } + } + + fun bind(meetingModel: CalendarMeetingModel) { + binding.meeting = meetingModel.calendarMeeting + val currentTime = Date().time + val showJoinButton = (meetingModel.calendarMeeting.startTime?.time ?: 0L <= currentTime && meetingModel.calendarMeeting.endTime?.time ?: 0L >= currentTime) || meetingModel.calendarMeeting.canJoin + binding.btnJoinMeeting.visibility = if (showJoinButton) View.VISIBLE else View.GONE + binding.tvTime.text = meetingModel.date + binding.btnJoinMeeting.setOnClickListener { + meetingJoinOptionsDialogFragment.meeting = meetingModel.calendarMeeting + meetingJoinOptionsDialogFragment.show(supportFragmentManager, "Calendar meeting join options") + } + binding.executePendingBindings() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingModel.kt new file mode 100644 index 0000000..306e323 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingModel.kt @@ -0,0 +1,16 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import java.text.SimpleDateFormat +import java.util.Date + +class CalendarMeetingModel constructor(val calendarMeeting: CalendarMeeting) { + var date = "${getFormattedTime(calendarMeeting.startTime)} - ${getFormattedTime(calendarMeeting.endTime)}" + + private fun getFormattedTime(date: Date?): String { + return if (date != null) { + val timeStampFormat = SimpleDateFormat("dd/MM/yyyy hh:mm a") + timeStampFormat.format(date) + } else "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsModule.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsModule.kt new file mode 100644 index 0000000..c9be5ab --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsModule.kt @@ -0,0 +1,12 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.details.CalendarMeetingDetailsViewModel +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val calendarMeetingsModule = module { + viewModel { CalendarMeetingsViewModel(get(), get()) } + viewModel { CalendarMeetingDetailsViewModel(get()) } + + single { CalendarMeetingsRepository(get()) } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsRepository.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsRepository.kt new file mode 100644 index 0000000..de709c8 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsRepository.kt @@ -0,0 +1,34 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.Webex +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import io.reactivex.Observable +import io.reactivex.Single +import java.util.Date + +class CalendarMeetingsRepository(private val webex: Webex) { + fun listCalendarMeetings(fromDate: Date?, toDate: Date?): Observable> { + return Single.create> { emitter -> + webex.calendarMeetings.list(fromDate, toDate, CompletionHandler { result -> + if (result.isSuccessful) { + emitter.onSuccess(result.data ?: emptyList()) + } else { + emitter.onError(Throwable(result.error?.errorMessage)) + } + }) + }.toObservable() + } + + fun getCalendarMeetingById(meetingId : String): Observable { + return Single.create { emitter -> + webex.calendarMeetings.getById(meetingId, CompletionHandler { result -> + if (result.isSuccessful) { + result.data?.let { emitter.onSuccess(it) } ?: emitter.onError(Throwable("No calendar meeting found!")) + } else { + emitter.onError(Throwable(result.error?.errorMessage)) + } + }) + }.toObservable() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsViewModel.kt new file mode 100644 index 0000000..8cada80 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/CalendarMeetingsViewModel.kt @@ -0,0 +1,66 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.ciscowebex.androidsdk.kitchensink.BaseViewModel +import com.ciscowebex.androidsdk.kitchensink.WebexRepository +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import com.ciscowebex.androidsdk.kitchensink.utils.getEndOfDay +import com.ciscowebex.androidsdk.kitchensink.utils.getStartOfDay +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.Date + +class CalendarMeetingsViewModel(private val repo: CalendarMeetingsRepository, private val webexRepository: WebexRepository) : BaseViewModel() { + private val _meetings = MutableLiveData>() + val meetings: LiveData> = _meetings + + var _calendarMeetingEventLiveData = MutableLiveData>() + + init { + webexRepository._calendarMeetingEventLiveData = _calendarMeetingEventLiveData + } + + override fun onCleared() { + webexRepository.removeCalendarMeetingObserver() + } + + fun getCalendarMeetingEvent() = webexRepository._calendarMeetingEventLiveData + + fun getCalendarMeetingsList(fromDate: Date? = null, toDate: Date? = null) { + repo.listCalendarMeetings(fromDate, toDate).observeOn(AndroidSchedulers.mainThread()) + .subscribe({ meetingsList -> + _meetings.postValue(meetingsList) + }, { _meetings.postValue(emptyList()) }).autoDispose() + } + + fun onFilterItemClick(filterByOption: FilterMeetingsBy) { + when (filterByOption) { + FilterMeetingsBy.Today -> { + val startTimeOfToday = getStartOfDay(Date()) + val endTimeOfToday = getEndOfDay(Date()) + getCalendarMeetingsList(startTimeOfToday, endTimeOfToday) + } + FilterMeetingsBy.Tomorrow -> { + val tomorrow = Date().time + 86400000 // Till next day example + val startTimeOfTomorrow = getStartOfDay(Date(tomorrow)) + val endTimeOfTomorrow = getEndOfDay(Date(tomorrow)) + getCalendarMeetingsList(startTimeOfTomorrow, endTimeOfTomorrow) + } + FilterMeetingsBy.PastMeetings -> { getCalendarMeetingsList(null, Date()) } + FilterMeetingsBy.UpcomingMeetings -> { + getCalendarMeetingsList(Date()) + } + FilterMeetingsBy.AllMeetings -> { + getCalendarMeetingsList() + } + } + } + + enum class FilterMeetingsBy { + Today, + Tomorrow, + PastMeetings, + UpcomingMeetings, + AllMeetings + } +} diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsActivity.kt new file mode 100644 index 0000000..2359be1 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsActivity.kt @@ -0,0 +1,99 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.details + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import com.ciscowebex.androidsdk.kitchensink.BaseActivity +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.CalendarMeetingModel +import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityCalendarMeetingDetailsBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.ListItemMeetingInviteeBinding +import com.ciscowebex.androidsdk.kitchensink.utils.Constants +import org.koin.android.ext.android.inject + +class CalendarMeetingDetailsActivity : BaseActivity() { + companion object { + fun getIntent(context: Context, meetingId: String): Intent { + val intent = Intent(context, CalendarMeetingDetailsActivity::class.java) + intent.putExtra(Constants.Intent.CALENDAR_MEETING_ID, meetingId) + return intent + } + } + + private lateinit var binding: ActivityCalendarMeetingDetailsBinding + private val meetingDetailsViewModel : CalendarMeetingDetailsViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val calendarMeetingId = intent.getStringExtra(Constants.Intent.CALENDAR_MEETING_ID) + DataBindingUtil.setContentView( + this, + R.layout.activity_calendar_meeting_details + ) + .apply { + binding = this + tvDescription.movementMethod = ScrollingMovementMethod() + setUpObservers() + } + meetingDetailsViewModel.getCalendarMeetingById(calendarMeetingId) + } + + private fun setUpObservers() { + meetingDetailsViewModel.meeting.observe(this@CalendarMeetingDetailsActivity, Observer { calendarMeeting -> + if (calendarMeeting != null) { + binding.meetingModel = CalendarMeetingModel((calendarMeeting)) + if (!calendarMeeting.invitees.isNullOrEmpty()) { + val rvAdapter = InviteesAdapter() + val invitees = calendarMeeting.invitees as MutableList + rvAdapter.invitees = invitees + binding.inviteesRecyclerView.adapter = rvAdapter + binding.tvInviteeCount.text = "(${invitees.size})" + + } else { + binding.inviteesRecyclerView.visibility = View.GONE + } + } + }) + } + + class InviteesAdapter : RecyclerView.Adapter() { + var invitees = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InviteesViewHolder { + return InviteesViewHolder( + ListItemMeetingInviteeBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ) + } + + override fun onBindViewHolder(holder: InviteesViewHolder, position: Int) { + holder.bind(invitees[position]) + } + + override fun getItemCount(): Int { + return invitees.size + } + + } + + class InviteesViewHolder(val binding: ListItemMeetingInviteeBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(invitee: CalendarMeeting.Invitee) { + binding.invitee = invitee + binding.executePendingBindings() + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsViewModel.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsViewModel.kt new file mode 100644 index 0000000..e3a426e --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/calling/calendarMeeting/details/CalendarMeetingDetailsViewModel.kt @@ -0,0 +1,24 @@ +package com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.details + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.ciscowebex.androidsdk.calendarMeeting.CalendarMeeting +import com.ciscowebex.androidsdk.kitchensink.BaseViewModel +import com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.CalendarMeetingsRepository +import io.reactivex.android.schedulers.AndroidSchedulers + +class CalendarMeetingDetailsViewModel(private val repo: CalendarMeetingsRepository) : BaseViewModel() { + private val tag = CalendarMeetingDetailsViewModel::class.java.name + private val _meeting = MutableLiveData() + val meeting: LiveData = _meeting + + fun getCalendarMeetingById(id: String) { + repo.getCalendarMeetingById(id).observeOn(AndroidSchedulers.mainThread()) + .subscribe({ meeting -> + _meeting.postValue(meeting) + }, { + Log.d(tag, it.message ?: "Error in getCalendarMeetingById api") + }).autoDispose() + } +} \ No newline at end of file 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 76e5bc8..b66eaa9 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 @@ -160,6 +160,9 @@ class SpaceDetailActivity : BaseActivity() { Log.d(tag, "Message Received event fired!") if(pair.second is Message) { val message = pair.second as Message + if (message.getId() != spaceId) { + return@Observer + } // For replies, find parent and add to replies list at bottom. if(message.isReply()){ val parentMessagePosition = messageClientAdapter.getPositionById(message.getParentId()?: "") @@ -181,7 +184,7 @@ class SpaceDetailActivity : BaseActivity() { } } }else { - messageClientAdapter.messages.add(SpaceMessageModel.convertToSpaceMessageModel(message)) + messageClientAdapter.messages.add(0, SpaceMessageModel.convertToSpaceMessageModel(message)) messageClientAdapter.notifyItemInserted(messageClientAdapter.messages.size - 1) } } diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchActivity.kt index 7ac937f..494b005 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchActivity.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/search/SearchActivity.kt @@ -8,9 +8,9 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.ciscowebex.androidsdk.kitchensink.BaseActivity import com.ciscowebex.androidsdk.kitchensink.R import com.ciscowebex.androidsdk.kitchensink.calling.DialFragment +import com.ciscowebex.androidsdk.kitchensink.calling.calendarMeeting.CalendarMeetingListFragment import com.ciscowebex.androidsdk.kitchensink.databinding.ActivitySearchBinding import com.ciscowebex.androidsdk.kitchensink.utils.Constants -import com.ciscowebex.androidsdk.kitchensink.utils.HorizontalFlipTransformation import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy @@ -27,7 +27,6 @@ class SearchActivity : BaseActivity() { .also { binding = it } .apply { viewPager.adapter = ViewPagerFragmentAdapter(this@SearchActivity, searchViewModel.titles) - viewPager.setPageTransformer(HorizontalFlipTransformation()) TabLayoutMediator(tabLayout, viewPager, TabConfigurationStrategy { tab: TabLayout.Tab, position: Int -> tab.text = searchViewModel.titles[position] @@ -62,6 +61,7 @@ class SearchActivity : BaseActivity() { spaceListFragment.arguments = bundle return spaceListFragment } + 4 -> return CalendarMeetingListFragment() } return SearchCommonFragment() } 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 4016a1a..0af5895 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 @@ -21,20 +21,7 @@ class SearchViewModel(private val searchRepo: SearchRepository, private val spac private val _spaceEventLiveData = MutableLiveData>() val titles = - listOf("Call", "Search", "History", "Spaces") - - var name = listOf( - "Bharath Balan", - "Adam Ranganathan", - "Rohit Sharma", - "Manoj Nuthakki", - "Linda Nixon", - "Akshay Agarwal", - "Lalit Sharma", - "Manu Jain", - "Ankit Batra", - "Jasna Ibrahim" - ) + listOf("Call", "Search", "History", "Spaces", "Meetings") init { webexRepo._spaceEventLiveData = _spaceEventLiveData diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/BackgroundOptionsBottomSheetFragment.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/BackgroundOptionsBottomSheetFragment.kt new file mode 100644 index 0000000..a249eb3 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/BackgroundOptionsBottomSheetFragment.kt @@ -0,0 +1,320 @@ +package com.ciscowebex.androidsdk.kitchensink.setup + +import android.app.Activity +import android.app.Dialog +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetVirtualBackgroundItemAddBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetVirtualBackgroundItemBlurBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetVirtualBackgroundItemImageBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetVirtualBackgroundItemNoneBinding +import com.ciscowebex.androidsdk.kitchensink.databinding.BottomSheetVirtualBackgroundsBinding +import com.ciscowebex.androidsdk.kitchensink.utils.FileUtils +import com.ciscowebex.androidsdk.kitchensink.utils.PermissionsHelper +import com.ciscowebex.androidsdk.phone.Phone +import com.ciscowebex.androidsdk.phone.VirtualBackground +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.gson.Gson +import org.koin.android.ext.android.inject +import java.io.File + + +class BackgroundOptionsBottomSheetFragment( + val onBackgroundChanged: (VirtualBackground) -> Unit, + val onBackgroundRemoved: (VirtualBackground) -> Unit, + val onNewBackgroundAdded: (File) -> Unit, + val onBottomSheetDimissed: () -> Unit +) : BottomSheetDialogFragment() { + + private lateinit var binding: BottomSheetVirtualBackgroundsBinding + val backgrounds: MutableList = arrayListOf() + private val PICKFILE_REQUEST_CODE = 10011 + private val permissionsHelper: PermissionsHelper by inject() + var adapter: BackgroundAdapter? = null + private val TAG: String = "BackgroundOptionsBottomSheetFragment" + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return BottomSheetVirtualBackgroundsBinding.inflate(inflater, container, false).also { binding = it }.apply { + adapter = BackgroundAdapter(dialog, backgrounds, onBackgroundChanged, + onBackgroundRemoved, { + val checkingPermission = checkReadStoragePermissions() + if (!checkingPermission) { + openFileExplorer() + } + }) + virtualBackgrounds.adapter = adapter + }.root + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + onBottomSheetDimissed() + } + + class BackgroundAdapter( + val dialog: Dialog?, + val virtualBackgrounds: MutableList, + val onBackgroundChanged: (VirtualBackground) -> Unit, + val onBackgroundRemoved: (VirtualBackground) -> Unit, + val onAddButtonClicked: () -> Unit + ) : RecyclerView.Adapter() { + private val itemTypeNone = 0 + private val itemTypeBlur = 1 + private val itemTypeImage = 2 + private val itemTypeAdd = 3 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when(viewType) { + itemTypeNone -> { + val view = BottomSheetVirtualBackgroundItemNoneBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ItemTypeNoneViewHolder(view, dialog, onBackgroundChanged) + } + itemTypeBlur -> { + val view = BottomSheetVirtualBackgroundItemBlurBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ItemTypeBlurViewHolder(view, dialog, onBackgroundChanged) + } + itemTypeImage -> { + val view = BottomSheetVirtualBackgroundItemImageBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ItemTypeImageViewHolder(view, dialog, onBackgroundChanged, onBackgroundRemoved) + } + itemTypeAdd -> { + val view = BottomSheetVirtualBackgroundItemAddBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ItemTypeAddViewHolder(view, dialog, onAddButtonClicked) + } + else -> { + val view = BottomSheetVirtualBackgroundItemNoneBinding.inflate( + LayoutInflater.from( + parent.context + ), parent, false + ) + ItemTypeNoneViewHolder(view, dialog, onBackgroundChanged) + } + } + } + + override fun getItemViewType(position: Int): Int { + return when(virtualBackgrounds[position].type) { + Phone.VirtualBackgroundType.NONE -> itemTypeNone + Phone.VirtualBackgroundType.BLUR -> itemTypeBlur + Phone.VirtualBackgroundType.CUSTOM -> itemTypeImage + else -> itemTypeAdd + } + } + + override fun getItemCount(): Int { + return virtualBackgrounds.size + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (getItemViewType(position)) { + itemTypeAdd -> (holder as ItemTypeAddViewHolder).bind() + itemTypeNone -> (holder as ItemTypeNoneViewHolder).bind() + itemTypeImage -> (holder as ItemTypeImageViewHolder).bind() + itemTypeBlur -> (holder as ItemTypeBlurViewHolder).bind() + } + } + + inner class ItemTypeNoneViewHolder( + val binding: BottomSheetVirtualBackgroundItemNoneBinding, + dialog: Dialog?, + val onBackgroundChanged: (VirtualBackground) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind() { + val item = virtualBackgrounds[adapterPosition] + if (item.isActive) { + binding.tvNone.foreground = + ContextCompat.getDrawable(binding.tvNone.context, R.drawable.border) + } else { + binding.tvNone.foreground = null + } + + binding.tvNone.setOnClickListener { + binding.tvNone.foreground = ContextCompat.getDrawable(binding.tvNone.context, R.drawable.border) + onBackgroundChanged(item) + dialog?.cancel() + } + } + } + inner class ItemTypeBlurViewHolder( + val binding: BottomSheetVirtualBackgroundItemBlurBinding, + dialog: Dialog?, + val onBackgroundChanged: (VirtualBackground) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind() { + val item = virtualBackgrounds[adapterPosition] + if (item.isActive) { + binding.tvBlur.foreground = + ContextCompat.getDrawable(binding.tvBlur.context, R.drawable.border) + } else { + binding.tvBlur.foreground = null + } + + binding.tvBlur.setOnClickListener { + binding.tvBlur.foreground = ContextCompat.getDrawable(binding.tvBlur.context, R.drawable.border) + onBackgroundChanged(item) + dialog?.cancel() + } + } + } + inner class ItemTypeImageViewHolder( + val binding: BottomSheetVirtualBackgroundItemImageBinding, + dialog: Dialog?, + val onBackgroundChanged: (VirtualBackground) -> Unit, + val onBackgroundRemoved: (VirtualBackground) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind() { + val item = virtualBackgrounds[adapterPosition] + if (item.isActive) { + binding.bgImg.foreground = + ContextCompat.getDrawable(binding.bgImg.context, R.drawable.border) + } else { + binding.bgImg.foreground = null + } + + binding.imgDelete.setOnClickListener { + onBackgroundRemoved(item) + } + + binding.bgImg.setOnClickListener { + binding.bgImg.foreground = ContextCompat.getDrawable(binding.bgImg.context, R.drawable.border) + onBackgroundChanged(item) + dialog?.cancel() + } + + val byteArray = virtualBackgrounds[adapterPosition].thumbnail?.thumbnail + if (byteArray != null) { + val bmp: Bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + Log.d("TAG", "bitmap: ${bmp.byteCount}") + binding.bgImg.setImageBitmap(bmp) + } + } + } + inner class ItemTypeAddViewHolder( + val binding: BottomSheetVirtualBackgroundItemAddBinding, + dialog: Dialog?, + val onAddButtonClicked: () -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind() { + binding.tvAdd.setOnClickListener { + onAddButtonClicked() + } + } + } + } + + private fun openFileExplorer() { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + startActivityForResult(intent, PICKFILE_REQUEST_CODE) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + handleActivityResult(requestCode, resultCode, data) + } + + private fun handleActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + if (requestCode == PICKFILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + data?.let { intent -> + intent.clipData?.let { data -> + for (index in 0 until data.itemCount) { + val uri = data.getItemAt(index).uri + addUriToList(uri) + } + } ?: run { + intent.data?.let { uri -> + addUriToList(uri) + } + } + } + } + } + + private fun addUriToList(uri: Uri) { + val filePath = FileUtils.getUploadUriPath(requireContext(), uri) + filePath?.let { + val file = File(it) + Log.d(TAG, "PICKFILE_REQUEST_CODE filePath: $it") + Log.d(TAG, "PICKFILE_REQUEST_CODE file Exist: ${file.exists()}") + + onNewBackgroundAdded(file) + } + } + + private fun checkReadStoragePermissions(): Boolean { + if (!permissionsHelper.hasReadStoragePermission()) { + Log.d(TAG, "requesting read permission") + requestPermissions( + PermissionsHelper.permissionForStorage(), + PermissionsHelper.PERMISSIONS_STORAGE_REQUEST + ) + return true + } else { + Log.d(TAG, "read permission granted") + } + return false + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + PermissionsHelper.PERMISSIONS_STORAGE_REQUEST -> { + if (PermissionsHelper.resultForCallingPermissions(permissions, grantResults)) { + Log.d(TAG, "read permission granted") + openFileExplorer() + } else { + Toast.makeText( + requireContext(), + getString(R.string.post_message_attach_permission_error), + Toast.LENGTH_LONG + ).show() + } + } + } + } + +} \ No newline at end of file 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 7e09918..85ce4c8 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 @@ -1,5 +1,6 @@ package com.ciscowebex.androidsdk.kitchensink.setup +import android.content.Intent import android.os.Bundle import android.util.Log import android.view.View @@ -44,20 +45,6 @@ class SetupActivity: BaseActivity() { } } - when (cameraCap) { - CameraCap.Front -> { - frontCamera.isChecked = true - setAndStartFrontCamera() - } - CameraCap.Back -> { - backCamera.isChecked = true - setAndStartBackCamera() - } - CameraCap.Close -> { - closePreview() - } - } - streamMode = webexViewModel.streamMode when (streamMode) { @@ -69,20 +56,6 @@ class SetupActivity: BaseActivity() { } } - cameraRadioGroup.setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - R.id.closePreview -> { - closePreview() - } - R.id.frontCamera -> { - setAndStartFrontCamera() - } - R.id.backCamera -> { - setAndStartBackCamera() - } - } - } - callCapabilityRadioGroup.setOnCheckedChangeListener { _, checkedId -> when (checkedId) { R.id.audioCallOnly -> { @@ -101,6 +74,13 @@ class SetupActivity: BaseActivity() { webexViewModel.enableBackgroundStream(checked) } + enableHWAccelToggle.isChecked = webexViewModel.enableHWAcceltoggle + + enableHWAccelToggle.setOnCheckedChangeListener { _, checked -> + webexViewModel.enableHWAcceltoggle = checked + webexViewModel.setHardwareAccelerationEnabled(checked) + } + streamModeRadioGroup.setOnCheckedChangeListener { _, checkedId -> when (checkedId) { R.id.composited -> { @@ -147,6 +127,10 @@ class SetupActivity: BaseActivity() { Log.d(tag, "enable console logger ${webexViewModel.isConsoleLoggerEnabled}") } switchConsoleLog.isChecked = webexViewModel.isConsoleLoggerEnabled + + cameraOptions.setOnClickListener { + startActivity(Intent(this@SetupActivity, SetupCameraActivity::class.java)) + } } } @@ -161,36 +145,4 @@ class SetupActivity: BaseActivity() { CameraCap.Back } } - - private fun closePreview() { - stopPreview() - } - - private fun setAndStartFrontCamera() { - webexViewModel.setDefaultFacingMode(Phone.FacingMode.USER) - cameraCap = CameraCap.Front - startPreview() - } - - private fun setAndStartBackCamera() { - webexViewModel.setDefaultFacingMode(Phone.FacingMode.ENVIROMENT) - cameraCap = CameraCap.Back - startPreview() - } - - private fun startPreview() { - binding.preview.visibility = View.VISIBLE - cameraCap = getDefaultCamera() - webexViewModel.startPreview(binding.preview) - } - - private fun stopPreview() { - webexViewModel.stopPreview() - binding.preview.visibility = View.GONE - } - - override fun onDestroy() { - webexViewModel.stopPreview() - super.onDestroy() - } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupCameraActivity.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupCameraActivity.kt new file mode 100644 index 0000000..8cdacd9 --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/setup/SetupCameraActivity.kt @@ -0,0 +1,208 @@ +package com.ciscowebex.androidsdk.kitchensink.setup + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import com.ciscowebex.androidsdk.CompletionHandler +import com.ciscowebex.androidsdk.kitchensink.BaseActivity +import com.ciscowebex.androidsdk.kitchensink.R +import com.ciscowebex.androidsdk.kitchensink.WebexRepository +import com.ciscowebex.androidsdk.kitchensink.databinding.ActivityCameraConfigBinding +import com.ciscowebex.androidsdk.kitchensink.utils.extensions.toast +import com.ciscowebex.androidsdk.message.LocalFile +import com.ciscowebex.androidsdk.phone.Phone +import com.ciscowebex.androidsdk.phone.VirtualBackground +import com.ciscowebex.androidsdk.utils.internal.MimeUtils +import java.io.File + +class SetupCameraActivity: BaseActivity() { + + enum class CameraCap { + Front, + Back, + Close + } + + lateinit var binding: ActivityCameraConfigBinding + private var cameraCap: CameraCap = CameraCap.Front + private lateinit var callCap: WebexRepository.CallCap + var bottomSheetFragment: BackgroundOptionsBottomSheetFragment? = null + + @SuppressLint("NotifyDataSetChanged") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + tag = "SetupActivity" + + DataBindingUtil.setContentView(this, R.layout.activity_camera_config) + .also { binding = it } + .apply { + cameraCap = getDefaultCamera() + + callCap = webexViewModel.callCapability + + when (cameraCap) { + CameraCap.Front -> { + frontCamera.isChecked = true + setAndStartFrontCamera() + } + CameraCap.Back -> { + backCamera.isChecked = true + setAndStartBackCamera() + } + CameraCap.Close -> { + closePreview() + } + } + + cameraRadioGroup.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.closePreview -> { + closePreview() + } + R.id.frontCamera -> { + setAndStartFrontCamera() + } + R.id.backCamera -> { + setAndStartBackCamera() + } + } + } + + changeBgButton.setOnClickListener { + webexViewModel.fetchVirtualBackgrounds() + } + + val limit = webexViewModel.getMaxVirtualBackgrounds() + textLimit.setText(limit.toString()) + setLimitButton.setOnClickListener { + val text = textLimit.text.toString() + if (text.isNotEmpty() && isNumber(text)) { + webexViewModel.setMaxVirtualBackgrounds(text.toInt()) + } else { + toast(getString(R.string.invalid_number_error)) + } + } + + webexViewModel.virtualBgError.observe(this@SetupCameraActivity, Observer { + toast(it) + }) + + webexViewModel.virtualBackground.observe(this@SetupCameraActivity, Observer { + Log.d(tag, "virtualBackgrounds size: ${it.size}") + val emptyBackground = VirtualBackground() + + if (bottomSheetFragment == null) { + bottomSheetFragment = + BackgroundOptionsBottomSheetFragment({ virtualBackground -> + if (!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, getString(R.string.virtual_bg_not_supported)) + Toast.makeText( + applicationContext, + getString(R.string.virtual_bg_not_supported), + Toast.LENGTH_SHORT + ) + .show() + return@BackgroundOptionsBottomSheetFragment + } + + webexViewModel.applyVirtualBackground( + virtualBackground, + Phone.VirtualBackgroundMode.PREVIEW) + }, + { virtualBackground -> + webexViewModel.removeVirtualBackground(virtualBackground) + }, + { file -> + if (!webexViewModel.isVirtualBackgroundSupported()) { + Log.d(tag, getString(R.string.virtual_bg_not_supported)) + Toast.makeText( + applicationContext, + getString(R.string.virtual_bg_not_supported), + Toast.LENGTH_SHORT + ) + .show() + return@BackgroundOptionsBottomSheetFragment + } + + val localFile = processAttachmentFile(file) + webexViewModel.addVirtualBackground(localFile) + }, + { + bottomSheetFragment = null + }) + + bottomSheetFragment?.show( + supportFragmentManager, + BackgroundOptionsBottomSheetFragment::class.java.name + ) + } + + bottomSheetFragment?.backgrounds?.clear() + bottomSheetFragment?.backgrounds?.addAll(it) + bottomSheetFragment?.backgrounds?.add(emptyBackground) + bottomSheetFragment?.adapter?.notifyDataSetChanged() + }) + } + } + + private fun processAttachmentFile(file: File): LocalFile { + var thumbnail: LocalFile.Thumbnail? = null + if (MimeUtils.getContentTypeByFilename(file.name) == MimeUtils.ContentType.IMAGE) { + thumbnail = LocalFile.Thumbnail(file, null, resources.getInteger(R.integer.virtual_bg_thumbnail_width), resources.getInteger(R.integer.virtual_bg_thumbnail_height)) + } + + return LocalFile(file, null, thumbnail, null) + } + + private fun getDefaultCamera(): CameraCap { + if (cameraCap == CameraCap.Close) { + return cameraCap + } + + return if (webexViewModel.getDefaultFacingMode() == Phone.FacingMode.USER) { + CameraCap.Front + } else { + CameraCap.Back + } + } + + private fun closePreview() { + stopPreview() + } + + private fun setAndStartFrontCamera() { + webexViewModel.setDefaultFacingMode(Phone.FacingMode.USER) + cameraCap = CameraCap.Front + startPreview() + } + + private fun setAndStartBackCamera() { + webexViewModel.setDefaultFacingMode(Phone.FacingMode.ENVIROMENT) + cameraCap = CameraCap.Back + startPreview() + } + + private fun startPreview() { + binding.preview.visibility = View.VISIBLE + cameraCap = getDefaultCamera() + webexViewModel.startPreview(binding.preview) + } + + private fun stopPreview() { + webexViewModel.stopPreview() + binding.preview.visibility = View.GONE + } + + override fun onDestroy() { + webexViewModel.stopPreview() + super.onDestroy() + } + + private fun isNumber(s: String?): Boolean { + return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) } + } +} \ No newline at end of file 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 b7c36ae..d2e28d9 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 @@ -12,6 +12,7 @@ class Constants { const val COMPOSER_REPLY_PARENT_MESSAGE = "composerReplyParentMessage" const val CALL_ID = "callid" const val MESSAGE_ID = "MESSAGE_ID" + const val CALENDAR_MEETING_ID = "CALENDAR_MEETING_ID" } object Bundle { const val MESSAGE_ID = "messageId" @@ -32,5 +33,6 @@ class Constants { const val KitchenSinkSharedPref = "KSSharedPref" const val LoginType = "LoginType" const val Email = "Email" + const val IsVirtualBgAdded = "IsVirtualBgAdded" } } \ No newline at end of file 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 new file mode 100644 index 0000000..bc1cdbb --- /dev/null +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/DateUtils.kt @@ -0,0 +1,29 @@ +package com.ciscowebex.androidsdk.kitchensink.utils + +import java.util.* + +fun getStartOfDay(date: Date): Date { + val cal = getCalInstance() + cal.time = date + cal.set(Calendar.HOUR_OF_DAY, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + + return cal.time +} + +fun getEndOfDay(date: Date): Date { + val cal = getCalInstance() + cal.time = date + cal.set(Calendar.HOUR_OF_DAY, 23) + cal.set(Calendar.MINUTE, 59) + cal.set(Calendar.SECOND, 59) + cal.set(Calendar.MILLISECOND, 999) + + return cal.time +} + +fun getCalInstance(): Calendar { + return Calendar.getInstance(Locale.getDefault()) +} \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/FileUtils.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/FileUtils.kt index 3747c7d..7211245 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/FileUtils.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/FileUtils.kt @@ -10,6 +10,12 @@ import android.provider.MediaStore import android.util.Log import com.ciscowebex.androidsdk.kitchensink.BuildConfig import java.io.File +import android.R +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException object FileUtils { @@ -124,4 +130,25 @@ object FileUtils { private fun isGooglePhotosUri(uri: Uri): Boolean { return "com.google.android.apps.photos.content" == uri.authority } + + fun getFileFromResource(context: Context, fileName: String): File { + val tempFile: File? + try { + val inputStream = context.resources.openRawResource(com.ciscowebex.androidsdk.kitchensink.R.raw.virtual_bg) + tempFile = File.createTempFile(fileName, ".jpg") + copyFile(inputStream, FileOutputStream(tempFile)) + + } catch (e: IOException) { + throw RuntimeException("Can't create temp file ", e) + } + return tempFile + } + + private fun copyFile(`in`: InputStream, out: OutputStream) { + val buffer = ByteArray(1024) + var read: Int + while (`in`.read(buffer).also { read = it } != -1) { + out.write(buffer, 0, read) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/HorizontalFlipTransformation.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/HorizontalFlipTransformation.kt deleted file mode 100644 index fe0f6e8..0000000 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/HorizontalFlipTransformation.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.ciscowebex.androidsdk.kitchensink.utils - -import android.view.View -import androidx.viewpager2.widget.ViewPager2 -import kotlin.math.abs - - -class HorizontalFlipTransformation : ViewPager2.PageTransformer { - override fun transformPage(page: View, position: Float) { - page.translationX = -position * page.width - page.cameraDistance = 12000F - if (position < 0.5 && position > -0.5) { - page.visibility = View.VISIBLE - } else { - page.visibility = View.INVISIBLE - } - if (position < -1) { // [-Infinity,-1) - page.alpha = 0F - } else if (position <= 0) { // [-1,0] - page.alpha = 1F - page.rotationY = 180 * (1 - abs(position) + 1) - } else if (position <= 1) { // (0,1] - page.alpha = 1F - page.rotationY = -180 * (1 - abs(position) + 1) - } else { - page.alpha = 0F - } - } -} \ 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 461d61d..66caea0 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 @@ -49,4 +49,19 @@ object SharedPrefUtils { return null } + + fun setVirtualBgAdded(context:Context, isAdded: Boolean) { + val pref = context.getSharedPreferences(Constants.Keys.KitchenSinkSharedPref, Context.MODE_PRIVATE) + pref?.edit()?.putBoolean(Constants.Keys.IsVirtualBgAdded, isAdded)?.apply() + } + + fun isVirtualBgAdded(context: Context): Boolean { + val pref = context.getSharedPreferences(Constants.Keys.KitchenSinkSharedPref, Context.MODE_PRIVATE) + + pref?.let { + return pref.getBoolean(Constants.Keys.IsVirtualBgAdded, false) + } + + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/extensions/ViewExtension.kt b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/extensions/ViewExtension.kt index d09ad59..b7d1d83 100644 --- a/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/extensions/ViewExtension.kt +++ b/app/src/main/java/com/ciscowebex/androidsdk/kitchensink/utils/extensions/ViewExtension.kt @@ -5,6 +5,7 @@ import android.content.Context import android.util.Log import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.Toast fun View.showKeyboard() { val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? @@ -16,4 +17,7 @@ fun Context.hideKeyboard(view: View) { val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) Log.d("ViewExtensions", "hideKeyboard()") -} \ No newline at end of file +} + +fun Context.toast(message: CharSequence) = + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() \ No newline at end of file diff --git a/app/src/main/res/drawable/border.xml b/app/src/main/res/drawable/border.xml new file mode 100644 index 0000000..cd7e3ed --- /dev/null +++ b/app/src/main/res/drawable/border.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_delete_24.xml b/app/src/main/res/drawable/ic_baseline_delete_24.xml new file mode 100644 index 0000000..ed540b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_delete_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000..46e7322 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_calendar_meeting_details.xml b/app/src/main/res/layout/activity_calendar_meeting_details.xml new file mode 100644 index 0000000..ec7fc56 --- /dev/null +++ b/app/src/main/res/layout/activity_calendar_meeting_details.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_camera_config.xml b/app/src/main/res/layout/activity_camera_config.xml new file mode 100644 index 0000000..5ce1816 --- /dev/null +++ b/app/src/main/res/layout/activity_camera_config.xml @@ -0,0 +1,110 @@ + + + + + + + +